Commit b4c63079 authored by Lukas 'Eipi' Eipert's avatar Lukas 'Eipi' Eipert Committed by Andrew Fontaine

Refactor Promote Milestone Modal

This refactors the "Promote Milestone Modal", replacing DeprecatedModal2
with GlModal. The way the modal is triggered, is changed from an
event-bus based solution to one which is "pure" Vue, simplifying the
code quite a bit. We moved most of the logic from the init function over
to the modal itself. Tests have been adjusted.
parent 66d56286
<script> <script>
import { GlModal } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
GlModal: DeprecatedModal2, GlModal,
},
props: {
milestoneTitle: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
groupName: {
type: String,
required: true,
}, },
data() {
return {
milestoneTitle: '',
url: '',
groupName: '',
currentButton: null,
visible: false,
};
}, },
computed: { computed: {
title() { title() {
...@@ -38,42 +32,71 @@ export default { ...@@ -38,42 +32,71 @@ export default {
); );
}, },
}, },
mounted() {
this.getButtons().forEach((button) => {
button.addEventListener('click', this.onPromoteButtonClick);
button.removeAttribute('disabled');
});
},
beforeDestroy() {
this.getButtons().forEach((button) => {
button.removeEventListener('click', this.onPromoteButtonClick);
});
},
methods: { methods: {
onPromoteButtonClick({ currentTarget }) {
const { milestoneTitle, url, groupName } = currentTarget.dataset;
currentTarget.setAttribute('disabled', '');
this.visible = true;
this.milestoneTitle = milestoneTitle;
this.url = url;
this.groupName = groupName;
this.currentButton = currentTarget;
},
getButtons() {
return document.querySelectorAll('.js-promote-project-milestone-button');
},
onSubmit() { onSubmit() {
eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
return axios return axios
.post(this.url, { params: { format: 'json' } }) .post(this.url, { params: { format: 'json' } })
.then((response) => { .then((response) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', {
milestoneUrl: this.url,
successful: true,
});
visitUrl(response.data.url); visitUrl(response.data.url);
}) })
.catch((error) => { .catch((error) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', {
milestoneUrl: this.url,
successful: false,
});
createFlash(error); createFlash(error);
})
.finally(() => {
this.visible = false;
}); });
}, },
onClose() {
this.visible = false;
if (this.currentButton) {
this.currentButton.removeAttribute('disabled');
}
},
},
primaryAction: {
text: s__('Milestones|Promote Milestone'),
attributes: [{ variant: 'warning' }],
},
cancelAction: {
text: s__('Cancel'),
attributes: [],
}, },
}; };
</script> </script>
<template> <template>
<gl-modal <gl-modal
id="promote-milestone-modal" :visible="visible"
:footer-primary-button-text="s__('Milestones|Promote Milestone')" modal-id="promote-milestone-modal"
footer-primary-button-variant="warning" :action-primary="$options.primaryAction"
@submit="onSubmit" :action-cancel="$options.cancelAction"
:title="title"
@primary="onSubmit"
@hide="onClose"
> >
<template #title>
{{ title }}
</template>
<div>
<p>{{ text }}</p> <p>{{ text }}</p>
<p>{{ s__('Milestones|This action cannot be reversed.') }}</p> <p>{{ s__('Milestones|This action cannot be reversed.') }}</p>
</div>
</gl-modal> </gl-modal>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import PromoteMilestoneModal from './components/promote_milestone_modal.vue'; import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
import eventHub from './event_hub';
Vue.use(Translate); Vue.use(Translate);
export default () => { export default () => {
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(
`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`,
);
if (!successful) {
button.removeAttribute('disabled');
}
};
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(
`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`,
);
button.setAttribute('disabled', '');
eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
url: button.dataset.url,
groupName: button.dataset.groupName,
};
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteMilestoneModal.props', modalProps);
};
const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button');
promoteMilestoneButtons.forEach((button) => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteMilestoneModal.mounted', () => {
promoteMilestoneButtons.forEach((button) => {
button.removeAttribute('disabled');
});
});
const promoteMilestoneModal = document.getElementById('promote-milestone-modal'); const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
let promoteMilestoneComponent; if (!promoteMilestoneModal) {
return null;
}
if (promoteMilestoneModal) { return new Vue({
promoteMilestoneComponent = new Vue({
el: promoteMilestoneModal, el: promoteMilestoneModal,
components: {
PromoteMilestoneModal,
},
data() {
return {
modalProps: {
milestoneTitle: '',
groupName: '',
url: '',
},
};
},
mounted() {
eventHub.$on('promoteMilestoneModal.props', this.setModalProps);
eventHub.$emit('promoteMilestoneModal.mounted');
},
beforeDestroy() {
eventHub.$off('promoteMilestoneModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) { render(createElement) {
return createElement('promote-milestone-modal', { return createElement(PromoteMilestoneModal);
props: this.modalProps,
});
}, },
}); });
}
return promoteMilestoneComponent;
}; };
...@@ -14,12 +14,9 @@ ...@@ -14,12 +14,9 @@
= link_to _('Edit'), edit_milestone_path(milestone), class: 'btn gl-button btn-grouped' = link_to _('Edit'), edit_milestone_path(milestone), class: 'btn gl-button btn-grouped'
- if milestone.project_milestone? && milestone.project.group - if milestone.project_milestone? && milestone.project.group
%button.js-promote-project-milestone-button.btn.gl-button.btn-grouped{ data: { toggle: 'modal', %button.js-promote-project-milestone-button.btn.gl-button.btn-grouped{ data: { milestone_title: milestone.title,
target: '#promote-milestone-modal',
milestone_title: milestone.title,
group_name: milestone.project.group.name, group_name: milestone.project.group.name,
url: promote_project_milestone_path(milestone.project, milestone), url: promote_project_milestone_path(milestone.project, milestone)},
container: 'body' },
disabled: true, disabled: true,
type: 'button' } type: 'button' }
= _('Promote') = _('Promote')
......
...@@ -51,10 +51,7 @@ ...@@ -51,10 +51,7 @@
type: 'button', type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone), data: { url: promote_project_milestone_path(milestone.project, milestone),
milestone_title: milestone.title, milestone_title: milestone.title,
group_name: @project.group.name, group_name: @project.group.name } }
target: '#promote-milestone-modal',
container: 'body',
toggle: 'modal' } }
= sprite_icon('level-up', size: 14) = sprite_icon('level-up', size: 14)
- if can?(current_user, :admin_milestone, milestone) - if can?(current_user, :admin_milestone, milestone)
......
import Vue from 'vue'; import { GlModal } from '@gitlab/ui';
import mountComponent from 'helpers/vue_mount_component_helper'; import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'jest/helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue'; import { setHTMLFixture } from 'helpers/fixtures';
import eventHub from '~/pages/milestones/shared/event_hub'; import waitForPromises from 'helpers/wait_for_promises';
import PromoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import * as flash from '~/flash';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
describe('Promote milestone modal', () => { describe('Promote milestone modal', () => {
let vm; let wrapper;
const Component = Vue.extend(promoteMilestoneModal);
const milestoneMockData = { const milestoneMockData = {
milestoneTitle: 'v1.0', milestoneTitle: 'v1.0',
url: `${TEST_HOST}/dummy/promote/milestones`, url: `${TEST_HOST}/dummy/promote/milestones`,
groupName: 'group', groupName: 'group',
}; };
describe('Modal title and description', () => { const promoteButton = () => document.querySelector('.js-promote-project-milestone-button');
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, milestoneMockData); setHTMLFixture(`<button
class="js-promote-project-milestone-button"
data-group-name="${milestoneMockData.groupName}"
data-milestone-title="${milestoneMockData.milestoneTitle}"
data-url="${milestoneMockData.url}">
Promote
</button>`);
wrapper = shallowMount(PromoteMilestoneModal);
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
});
describe('Modal opener button', () => {
it('button gets disabled when the modal opens', () => {
expect(promoteButton().disabled).toBe(false);
promoteButton().click();
expect(promoteButton().disabled).toBe(true);
});
it('button gets enabled when the modal closes', () => {
promoteButton().click();
wrapper.findComponent(GlModal).vm.$emit('hide');
expect(promoteButton().disabled).toBe(false);
});
});
describe('Modal title and description', () => {
beforeEach(() => {
promoteButton().click();
}); });
it('contains the proper description', () => { it('contains the proper description', () => {
expect(vm.text).toContain( expect(wrapper.vm.text).toContain(
`Promoting ${milestoneMockData.milestoneTitle} will make it available for all projects inside ${milestoneMockData.groupName}.`, `Promoting ${milestoneMockData.milestoneTitle} will make it available for all projects inside ${milestoneMockData.groupName}.`,
); );
}); });
it('contains the correct title', () => { it('contains the correct title', () => {
expect(vm.title).toEqual('Promote v1.0 to group milestone?'); expect(wrapper.vm.title).toBe('Promote v1.0 to group milestone?');
}); });
}); });
describe('When requesting a milestone promotion', () => { describe('When requesting a milestone promotion', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { promoteButton().click();
...milestoneMockData,
});
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
afterEach(() => {
vm.$destroy();
}); });
it('redirects when a milestone is promoted', (done) => { it('redirects when a milestone is promoted', async () => {
const responseURL = `${TEST_HOST}/dummy/endpoint`; const responseURL = `${TEST_HOST}/dummy/endpoint`;
jest.spyOn(axios, 'post').mockImplementation((url) => { jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(milestoneMockData.url); expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteMilestoneModal.requestStarted',
milestoneMockData.url,
);
return Promise.resolve({ return Promise.resolve({
request: { data: {
responseURL, url: responseURL,
}, },
}); });
}); });
vm.onSubmit() wrapper.findComponent(GlModal).vm.$emit('primary');
.then(() => { await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', {
milestoneUrl: milestoneMockData.url, expect(urlUtils.visitUrl).toHaveBeenCalledWith(responseURL);
successful: true,
});
})
.then(done)
.catch(done.fail);
}); });
it('displays an error if promoting a milestone failed', (done) => { it('displays an error if promoting a milestone failed', async () => {
const dummyError = new Error('promoting milestone failed'); const dummyError = new Error('promoting milestone failed');
dummyError.response = { status: 500 }; dummyError.response = { status: 500 };
jest.spyOn(axios, 'post').mockImplementation((url) => { jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(milestoneMockData.url); expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteMilestoneModal.requestStarted',
milestoneMockData.url,
);
return Promise.reject(dummyError); return Promise.reject(dummyError);
}); });
vm.onSubmit() wrapper.findComponent(GlModal).vm.$emit('primary');
.catch((error) => { await waitForPromises();
expect(error).toBe(dummyError);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', { expect(flash.deprecatedCreateFlash).toHaveBeenCalledWith(dummyError);
milestoneUrl: milestoneMockData.url,
successful: false,
});
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
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