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>
import { GlModal } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
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: {
title() {
......@@ -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: {
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() {
eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
return axios
.post(this.url, { params: { format: 'json' } })
.then((response) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', {
milestoneUrl: this.url,
successful: true,
});
visitUrl(response.data.url);
})
.catch((error) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', {
milestoneUrl: this.url,
successful: false,
});
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>
<template>
<gl-modal
id="promote-milestone-modal"
:footer-primary-button-text="s__('Milestones|Promote Milestone')"
footer-primary-button-variant="warning"
@submit="onSubmit"
:visible="visible"
modal-id="promote-milestone-modal"
:action-primary="$options.primaryAction"
:action-cancel="$options.cancelAction"
:title="title"
@primary="onSubmit"
@hide="onClose"
>
<template #title>
{{ title }}
</template>
<div>
<p>{{ text }}</p>
<p>{{ s__('Milestones|This action cannot be reversed.') }}</p>
</div>
<p>{{ text }}</p>
<p>{{ s__('Milestones|This action cannot be reversed.') }}</p>
</gl-modal>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
import eventHub from './event_hub';
Vue.use(Translate);
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');
let promoteMilestoneComponent;
if (promoteMilestoneModal) {
promoteMilestoneComponent = new Vue({
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) {
return createElement('promote-milestone-modal', {
props: this.modalProps,
});
},
});
if (!promoteMilestoneModal) {
return null;
}
return promoteMilestoneComponent;
return new Vue({
el: promoteMilestoneModal,
render(createElement) {
return createElement(PromoteMilestoneModal);
},
});
};
......@@ -14,12 +14,9 @@
= link_to _('Edit'), edit_milestone_path(milestone), class: 'btn gl-button btn-grouped'
- if milestone.project_milestone? && milestone.project.group
%button.js-promote-project-milestone-button.btn.gl-button.btn-grouped{ data: { toggle: 'modal',
target: '#promote-milestone-modal',
milestone_title: milestone.title,
%button.js-promote-project-milestone-button.btn.gl-button.btn-grouped{ data: { milestone_title: milestone.title,
group_name: milestone.project.group.name,
url: promote_project_milestone_path(milestone.project, milestone),
container: 'body' },
url: promote_project_milestone_path(milestone.project, milestone)},
disabled: true,
type: 'button' }
= _('Promote')
......
......@@ -51,10 +51,7 @@
type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone),
milestone_title: milestone.title,
group_name: @project.group.name,
target: '#promote-milestone-modal',
container: 'body',
toggle: 'modal' } }
group_name: @project.group.name } }
= sprite_icon('level-up', size: 14)
- if can?(current_user, :admin_milestone, milestone)
......
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'jest/helpers/test_constants';
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { setHTMLFixture } from 'helpers/fixtures';
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 * 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', () => {
let vm;
const Component = Vue.extend(promoteMilestoneModal);
let wrapper;
const milestoneMockData = {
milestoneTitle: 'v1.0',
url: `${TEST_HOST}/dummy/promote/milestones`,
groupName: 'group',
};
describe('Modal title and description', () => {
beforeEach(() => {
vm = mountComponent(Component, milestoneMockData);
const promoteButton = () => document.querySelector('.js-promote-project-milestone-button');
beforeEach(() => {
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(() => {
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);
});
});
afterEach(() => {
vm.$destroy();
describe('Modal title and description', () => {
beforeEach(() => {
promoteButton().click();
});
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}.`,
);
});
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', () => {
beforeEach(() => {
vm = mountComponent(Component, {
...milestoneMockData,
});
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
afterEach(() => {
vm.$destroy();
promoteButton().click();
});
it('redirects when a milestone is promoted', (done) => {
it('redirects when a milestone is promoted', async () => {
const responseURL = `${TEST_HOST}/dummy/endpoint`;
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteMilestoneModal.requestStarted',
milestoneMockData.url,
);
return Promise.resolve({
request: {
responseURL,
data: {
url: responseURL,
},
});
});
vm.onSubmit()
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', {
milestoneUrl: milestoneMockData.url,
successful: true,
});
})
.then(done)
.catch(done.fail);
wrapper.findComponent(GlModal).vm.$emit('primary');
await waitForPromises();
expect(urlUtils.visitUrl).toHaveBeenCalledWith(responseURL);
});
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');
dummyError.response = { status: 500 };
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteMilestoneModal.requestStarted',
milestoneMockData.url,
);
return Promise.reject(dummyError);
});
vm.onSubmit()
.catch((error) => {
expect(error).toBe(dummyError);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', {
milestoneUrl: milestoneMockData.url,
successful: false,
});
})
.then(done)
.catch(done.fail);
wrapper.findComponent(GlModal).vm.$emit('primary');
await waitForPromises();
expect(flash.deprecatedCreateFlash).toHaveBeenCalledWith(dummyError);
});
});
});
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