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> <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: { data() {
milestoneTitle: { return {
type: String, milestoneTitle: '',
required: true, url: '',
}, groupName: '',
url: { currentButton: null,
type: String, visible: false,
required: true, };
},
groupName: {
type: String,
required: true,
},
}, },
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> <p>{{ text }}</p>
{{ title }} <p>{{ s__('Milestones|This action cannot be reversed.') }}</p>
</template>
<div>
<p>{{ text }}</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) {
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,
});
},
});
} }
return promoteMilestoneComponent; return new Vue({
el: promoteMilestoneModal,
render(createElement) {
return createElement(PromoteMilestoneModal);
},
});
}; };
...@@ -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(() => {
vm = mountComponent(Component, milestoneMockData); 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(() => { describe('Modal title and description', () => {
vm.$destroy(); 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