Merge branch 'jivl-new-modal-project-labels-milestones' into 'master'
Resolve: Modal and banner designs for project label and project milestone promotion Closes #41895 See merge request gitlab-org/gitlab-ce!17197
This commit is contained in:
commit
ce819eceea
|
@ -0,0 +1,64 @@
|
|||
<script>
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import createFlash from '~/flash';
|
||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
},
|
||||
props: {
|
||||
milestoneTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
|
||||
},
|
||||
text() {
|
||||
return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
|
||||
Existing project milestones with the same title will be merged.
|
||||
This action cannot be reversed.`);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
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);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal
|
||||
id="promote-milestone-modal"
|
||||
footer-primary-button-variant="warning"
|
||||
:footer-primary-button-text="s__('Milestones|Promote Milestone')"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template
|
||||
slot="title"
|
||||
>
|
||||
{{ title }}
|
||||
</template>
|
||||
{{ text }}
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
|
||||
import eventHub from './event_hub';
|
||||
|
||||
export default () => {
|
||||
Vue.use(Translate);
|
||||
|
||||
const onRequestFinished = ({ milestoneUrl, successful }) => {
|
||||
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
|
||||
|
||||
if (!successful) {
|
||||
button.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
button.querySelector('.js-loading-icon').classList.add('hidden');
|
||||
};
|
||||
|
||||
const onRequestStarted = (milestoneUrl) => {
|
||||
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
|
||||
button.setAttribute('disabled', '');
|
||||
button.querySelector('.js-loading-icon').classList.remove('hidden');
|
||||
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
|
||||
};
|
||||
|
||||
const onDeleteButtonClick = (event) => {
|
||||
const button = event.currentTarget;
|
||||
const modalProps = {
|
||||
milestoneId: parseInt(button.dataset.milestoneId, 10),
|
||||
milestoneTitle: button.dataset.milestoneTitle,
|
||||
milestoneUrl: button.dataset.milestoneUrl,
|
||||
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
|
||||
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
|
||||
};
|
||||
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
|
||||
eventHub.$emit('deleteMilestoneModal.props', modalProps);
|
||||
};
|
||||
|
||||
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
|
||||
deleteMilestoneButtons.forEach((button) => {
|
||||
button.addEventListener('click', onDeleteButtonClick);
|
||||
});
|
||||
|
||||
eventHub.$once('deleteMilestoneModal.mounted', () => {
|
||||
deleteMilestoneButtons.forEach((button) => {
|
||||
button.removeAttribute('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el: '#delete-milestone-modal',
|
||||
components: {
|
||||
deleteMilestoneModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalProps: {
|
||||
milestoneId: -1,
|
||||
milestoneTitle: '',
|
||||
milestoneUrl: '',
|
||||
issueCount: -1,
|
||||
mergeRequestCount: -1,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
|
||||
eventHub.$emit('deleteMilestoneModal.mounted');
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
|
||||
},
|
||||
methods: {
|
||||
setModalProps(modalProps) {
|
||||
this.modalProps = modalProps;
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(deleteMilestoneModal, {
|
||||
props: this.modalProps,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,88 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import Translate from '~/vue_shared/translate';
|
||||
|
||||
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
|
||||
import eventHub from './event_hub';
|
||||
import initDeleteMilestoneModal from './delete_milestone_modal_init';
|
||||
import initPromoteMilestoneModal from './promote_milestone_modal_init';
|
||||
|
||||
export default () => {
|
||||
Vue.use(Translate);
|
||||
|
||||
const onRequestFinished = ({ milestoneUrl, successful }) => {
|
||||
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
|
||||
|
||||
if (!successful) {
|
||||
button.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
button.querySelector('.js-loading-icon').classList.add('hidden');
|
||||
};
|
||||
|
||||
const onRequestStarted = (milestoneUrl) => {
|
||||
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
|
||||
button.setAttribute('disabled', '');
|
||||
button.querySelector('.js-loading-icon').classList.remove('hidden');
|
||||
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
|
||||
};
|
||||
|
||||
const onDeleteButtonClick = (event) => {
|
||||
const button = event.currentTarget;
|
||||
const modalProps = {
|
||||
milestoneId: parseInt(button.dataset.milestoneId, 10),
|
||||
milestoneTitle: button.dataset.milestoneTitle,
|
||||
milestoneUrl: button.dataset.milestoneUrl,
|
||||
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
|
||||
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
|
||||
};
|
||||
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
|
||||
eventHub.$emit('deleteMilestoneModal.props', modalProps);
|
||||
};
|
||||
|
||||
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
|
||||
for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
|
||||
const button = deleteMilestoneButtons[i];
|
||||
button.addEventListener('click', onDeleteButtonClick);
|
||||
}
|
||||
|
||||
eventHub.$once('deleteMilestoneModal.mounted', () => {
|
||||
for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
|
||||
const button = deleteMilestoneButtons[i];
|
||||
button.removeAttribute('disabled');
|
||||
}
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el: '#delete-milestone-modal',
|
||||
components: {
|
||||
deleteMilestoneModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalProps: {
|
||||
milestoneId: -1,
|
||||
milestoneTitle: '',
|
||||
milestoneUrl: '',
|
||||
issueCount: -1,
|
||||
mergeRequestCount: -1,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
|
||||
eventHub.$emit('deleteMilestoneModal.mounted');
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
|
||||
},
|
||||
methods: {
|
||||
setModalProps(modalProps) {
|
||||
this.modalProps = modalProps;
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(deleteMilestoneModal, {
|
||||
props: this.modalProps,
|
||||
});
|
||||
},
|
||||
});
|
||||
initDeleteMilestoneModal();
|
||||
initPromoteMilestoneModal();
|
||||
};
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
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,
|
||||
};
|
||||
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: '',
|
||||
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;
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
<script>
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import createFlash from '~/flash';
|
||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelColor: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelTextColor: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
text() {
|
||||
return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
|
||||
Existing project labels with the same title will be merged. This action cannot be reversed.`);
|
||||
},
|
||||
title() {
|
||||
const label = `<span
|
||||
class="label color-label"
|
||||
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
|
||||
>${this.labelTitle}</span>`;
|
||||
|
||||
return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
|
||||
labelTitle: label,
|
||||
}, false);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
eventHub.$emit('promoteLabelModal.requestStarted', this.url);
|
||||
return axios.post(this.url, { params: { format: 'json' } })
|
||||
.then((response) => {
|
||||
eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true });
|
||||
visitUrl(response.data.url);
|
||||
})
|
||||
.catch((error) => {
|
||||
eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false });
|
||||
createFlash(error);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal
|
||||
id="promote-label-modal"
|
||||
footer-primary-button-variant="warning"
|
||||
:footer-primary-button-text="s__('Labels|Promote Label')"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<div
|
||||
slot="title"
|
||||
v-html="title"
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
|
||||
{{ text }}
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -0,0 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default new Vue();
|
|
@ -1,3 +1,91 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import initLabels from '~/init_labels';
|
||||
import eventHub from '../event_hub';
|
||||
import PromoteLabelModal from '../components/promote_label_modal.vue';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initLabels);
|
||||
Vue.use(Translate);
|
||||
|
||||
const initLabelIndex = () => {
|
||||
initLabels();
|
||||
|
||||
const onRequestFinished = ({ labelUrl, successful }) => {
|
||||
const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
|
||||
|
||||
if (!successful) {
|
||||
button.removeAttribute('disabled');
|
||||
}
|
||||
};
|
||||
|
||||
const onRequestStarted = (labelUrl) => {
|
||||
const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
|
||||
button.setAttribute('disabled', '');
|
||||
eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished);
|
||||
};
|
||||
|
||||
const onDeleteButtonClick = (event) => {
|
||||
const button = event.currentTarget;
|
||||
const modalProps = {
|
||||
labelTitle: button.dataset.labelTitle,
|
||||
labelColor: button.dataset.labelColor,
|
||||
labelTextColor: button.dataset.labelTextColor,
|
||||
url: button.dataset.url,
|
||||
};
|
||||
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
|
||||
eventHub.$emit('promoteLabelModal.props', modalProps);
|
||||
};
|
||||
|
||||
const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button');
|
||||
promoteLabelButtons.forEach((button) => {
|
||||
button.addEventListener('click', onDeleteButtonClick);
|
||||
});
|
||||
|
||||
eventHub.$once('promoteLabelModal.mounted', () => {
|
||||
promoteLabelButtons.forEach((button) => {
|
||||
button.removeAttribute('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
const promoteLabelModal = document.getElementById('promote-label-modal');
|
||||
let promoteLabelModalComponent;
|
||||
|
||||
if (promoteLabelModal) {
|
||||
promoteLabelModalComponent = new Vue({
|
||||
el: promoteLabelModal,
|
||||
components: {
|
||||
PromoteLabelModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalProps: {
|
||||
labelTitle: '',
|
||||
labelColor: '',
|
||||
labelTextColor: '',
|
||||
url: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('promoteLabelModal.props', this.setModalProps);
|
||||
eventHub.$emit('promoteLabelModal.mounted');
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('promoteLabelModal.props', this.setModalProps);
|
||||
},
|
||||
methods: {
|
||||
setModalProps(modalProps) {
|
||||
this.modalProps = modalProps;
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('promote-label-modal', {
|
||||
props: this.modalProps,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return promoteLabelModalComponent;
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initLabelIndex);
|
||||
|
|
|
@ -2,14 +2,17 @@
|
|||
background-color: $modal-body-bg;
|
||||
padding: #{3 * $grid-size} #{2 * $grid-size};
|
||||
|
||||
.page-title {
|
||||
margin-top: 0;
|
||||
|
||||
.page-title,
|
||||
.modal-title {
|
||||
.color-label {
|
||||
font-size: $gl-font-size;
|
||||
padding: $gl-vert-padding $label-padding-modal;
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
|
|
|
@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController
|
|||
begin
|
||||
return render_404 unless promote_service.execute(@label)
|
||||
|
||||
flash[:notice] = "#{@label.title} promoted to group label."
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to(project_labels_path(@project),
|
||||
notice: 'Label was promoted to a Group Label')
|
||||
redirect_to(project_labels_path(@project), status: 303)
|
||||
end
|
||||
format.json do
|
||||
render json: { url: project_labels_path(@project) }
|
||||
end
|
||||
format.js
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
|
||||
|
|
|
@ -70,9 +70,17 @@ class Projects::MilestonesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def promote
|
||||
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
|
||||
flash[:notice] = "Milestone has been promoted to group milestone."
|
||||
redirect_to group_milestone_path(project.group, promoted_milestone.iid)
|
||||
Milestones::PromoteService.new(project, current_user).execute(milestone)
|
||||
|
||||
flash[:notice] = "#{milestone.title} promoted to group milestone"
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to project_milestones_path(project)
|
||||
end
|
||||
format.json do
|
||||
render json: { url: project_milestones_path(project) }
|
||||
end
|
||||
end
|
||||
rescue Milestones::PromoteService::PromoteMilestoneError => error
|
||||
redirect_to milestone, alert: error.message
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
- can_admin_label = can?(current_user, :admin_label, @project)
|
||||
|
||||
- if @labels.exists? || @prioritized_labels.exists?
|
||||
#promote-label-modal
|
||||
%div{ class: container_class }
|
||||
.top-area.adjust
|
||||
.nav-text
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
.milestones
|
||||
#delete-milestone-modal
|
||||
#promote-milestone-modal
|
||||
|
||||
%ul.content-list
|
||||
= render @milestones
|
||||
|
|
|
@ -27,8 +27,15 @@
|
|||
Edit
|
||||
|
||||
- if @project.group
|
||||
= link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting #{@milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do
|
||||
Promote
|
||||
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
|
||||
target: '#promote-milestone-modal',
|
||||
milestone_title: @milestone.title,
|
||||
url: promote_project_milestone_path(@milestone.project, @milestone),
|
||||
container: 'body' },
|
||||
disabled: true,
|
||||
type: 'button' }
|
||||
= _('Promote')
|
||||
#promote-milestone-modal
|
||||
|
||||
- if @milestone.active?
|
||||
= link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
|
||||
|
|
|
@ -48,8 +48,16 @@
|
|||
|
||||
.pull-right.hidden-xs.hidden-sm
|
||||
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
|
||||
= link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting #{label.title} will make it available for all projects inside #{label.project.group.name}. Existing project labels with the same name will be merged. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
|
||||
%span.sr-only Promote to Group
|
||||
%button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
|
||||
disabled: true,
|
||||
type: 'button',
|
||||
data: { url: promote_project_label_path(label.project, label),
|
||||
label_title: label.title,
|
||||
label_color: label.color,
|
||||
label_text_color: label.text_color,
|
||||
target: '#promote-label-modal',
|
||||
container: 'body',
|
||||
toggle: 'modal' } }
|
||||
= sprite_icon('level-up')
|
||||
- if can?(current_user, :admin_label, label)
|
||||
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
|
||||
|
|
|
@ -51,8 +51,15 @@
|
|||
\
|
||||
|
||||
- if @project.group
|
||||
= link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting #{milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do
|
||||
Promote
|
||||
%button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
|
||||
disabled: true,
|
||||
type: 'button',
|
||||
data: { url: promote_project_milestone_path(milestone.project, milestone),
|
||||
milestone_title: milestone.title,
|
||||
target: '#promote-milestone-modal',
|
||||
container: 'body',
|
||||
toggle: 'modal' } }
|
||||
= _('Promote')
|
||||
|
||||
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added new design for promotion modals
|
||||
merge_request: 17197
|
||||
author:
|
||||
type: other
|
|
@ -98,10 +98,8 @@ describe Projects::MilestonesController do
|
|||
it 'shows group milestone' do
|
||||
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
|
||||
|
||||
group_milestone = assigns(:milestone)
|
||||
|
||||
expect(response).to redirect_to(group_milestone_path(project.group, group_milestone.iid))
|
||||
expect(flash[:notice]).to eq('Milestone has been promoted to group milestone.')
|
||||
expect(flash[:notice]).to eq("#{milestone.title} promoted to group milestone")
|
||||
expect(response).to redirect_to(project_milestones_path(project))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import Vue from 'vue';
|
||||
import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
|
||||
import eventHub from '~/pages/projects/labels/event_hub';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import mountComponent from '../../../helpers/vue_mount_component_helper';
|
||||
|
||||
describe('Promote label modal', () => {
|
||||
let vm;
|
||||
const Component = Vue.extend(promoteLabelModal);
|
||||
const labelMockData = {
|
||||
labelTitle: 'Documentation',
|
||||
labelColor: '#5cb85c',
|
||||
labelTextColor: '#ffffff',
|
||||
url: `${gl.TEST_HOST}/dummy/promote/labels`,
|
||||
};
|
||||
|
||||
describe('Modal title and description', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component, labelMockData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('contains the proper description', () => {
|
||||
expect(vm.text).toContain('Promoting this label will make it available for all projects inside the group');
|
||||
});
|
||||
|
||||
it('contains a label span with the color', () => {
|
||||
const labelFromTitle = vm.$el.querySelector('.modal-header .label.color-label');
|
||||
|
||||
expect(labelFromTitle.style.backgroundColor).not.toBe(null);
|
||||
expect(labelFromTitle.textContent).toContain(vm.labelTitle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When requesting a label promotion', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component, {
|
||||
...labelMockData,
|
||||
});
|
||||
spyOn(eventHub, '$emit');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('redirects when a label is promoted', (done) => {
|
||||
const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
|
||||
spyOn(axios, 'post').and.callFake((url) => {
|
||||
expect(url).toBe(labelMockData.url);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestStarted', labelMockData.url);
|
||||
return Promise.resolve({
|
||||
request: {
|
||||
responseURL,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
vm.onSubmit()
|
||||
.then(() => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { labelUrl: labelMockData.url, successful: true });
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('displays an error if promoting a label failed', (done) => {
|
||||
const dummyError = new Error('promoting label failed');
|
||||
dummyError.response = { status: 500 };
|
||||
spyOn(axios, 'post').and.callFake((url) => {
|
||||
expect(url).toBe(labelMockData.url);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestStarted', labelMockData.url);
|
||||
return Promise.reject(dummyError);
|
||||
});
|
||||
|
||||
vm.onSubmit()
|
||||
.catch((error) => {
|
||||
expect(error).toBe(dummyError);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { labelUrl: labelMockData.url, successful: false });
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
import Vue from 'vue';
|
||||
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
|
||||
import eventHub from '~/pages/milestones/shared/event_hub';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import mountComponent from '../../../../helpers/vue_mount_component_helper';
|
||||
|
||||
describe('Promote milestone modal', () => {
|
||||
let vm;
|
||||
const Component = Vue.extend(promoteMilestoneModal);
|
||||
const milestoneMockData = {
|
||||
milestoneTitle: 'v1.0',
|
||||
url: `${gl.TEST_HOST}/dummy/promote/milestones`,
|
||||
};
|
||||
|
||||
describe('Modal title and description', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component, milestoneMockData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('contains the proper description', () => {
|
||||
expect(vm.text).toContain('Promoting this milestone will make it available for all projects inside the group.');
|
||||
});
|
||||
|
||||
it('contains the correct title', () => {
|
||||
expect(vm.title).toEqual('Promote v1.0 to group milestone?');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When requesting a milestone promotion', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component, {
|
||||
...milestoneMockData,
|
||||
});
|
||||
spyOn(eventHub, '$emit');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('redirects when a milestone is promoted', (done) => {
|
||||
const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
|
||||
spyOn(axios, 'post').and.callFake((url) => {
|
||||
expect(url).toBe(milestoneMockData.url);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestStarted', milestoneMockData.url);
|
||||
return Promise.resolve({
|
||||
request: {
|
||||
responseURL,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
vm.onSubmit()
|
||||
.then(() => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', { milestoneUrl: milestoneMockData.url, successful: true });
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('displays an error if promoting a milestone failed', (done) => {
|
||||
const dummyError = new Error('promoting milestone failed');
|
||||
dummyError.response = { status: 500 };
|
||||
spyOn(axios, 'post').and.callFake((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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue