Commit 35522f85 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '229702-refactor-milestone-promotion-modal' into 'master'

Refactor Promote Milestone Modal

See merge request gitlab-org/gitlab!51227
parents 326e793d b4c63079
<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