From d5c001864964afb73efca0c6b592f8a1e4d152b9 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 16 Feb 2018 18:11:36 -0600 Subject: [PATCH 1/4] Added vue based promotion modals for labels and milestones --- .../components/promote_milestone_modal.vue | 64 ++++++++++++ .../shared/delete_milestone_modal_init.js | 86 ++++++++++++++++ .../pages/milestones/shared/index.js | 89 +---------------- .../shared/promote_milestone_modal_init.js | 82 +++++++++++++++ .../labels/components/promote_label_modal.vue | 71 +++++++++++++ .../pages/projects/labels/event_hub.js | 3 + .../pages/projects/labels/index/index.js | 88 ++++++++++++++++- app/assets/stylesheets/framework/modal.scss | 3 +- app/controllers/projects/labels_controller.rb | 3 +- .../projects/milestones_controller.rb | 3 +- app/views/projects/labels/index.html.haml | 1 + app/views/projects/milestones/index.html.haml | 1 + app/views/projects/milestones/show.html.haml | 10 +- app/views/shared/_label.html.haml | 10 +- .../shared/milestones/_milestone.html.haml | 8 +- ...vl-new-modal-project-labels-milestones.yml | 5 + .../projects/milestones_controller_spec.rb | 1 - .../components/promote_label_modal_spec.js | 99 +++++++++++++++++++ .../promote_milestone_modal_spec.js | 95 ++++++++++++++++++ 19 files changed, 625 insertions(+), 97 deletions(-) create mode 100644 app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue create mode 100644 app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js create mode 100644 app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js create mode 100644 app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue create mode 100644 app/assets/javascripts/pages/projects/labels/event_hub.js create mode 100644 changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml create mode 100644 spec/javascripts/pages/labels/components/promote_label_modal_spec.js create mode 100644 spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue new file mode 100644 index 00000000000..4f9be4f4f99 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -0,0 +1,64 @@ + + + diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js new file mode 100644 index 00000000000..326673f3a2f --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js @@ -0,0 +1,86 @@ +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'); + 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, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js index 327e2cf569c..dabfe32848b 100644 --- a/app/assets/javascripts/pages/milestones/shared/index.js +++ b/app/assets/javascripts/pages/milestones/shared/index.js @@ -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(); }; diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js new file mode 100644 index 00000000000..ea9adb9fe76 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js @@ -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); + +const onRequestFinished = ({ milestoneUrl, successful }) => { + const button = document.querySelector(`.js-promote-project-milestone[data-url="${milestoneUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } +}; + +const onRequestStarted = (milestoneUrl) => { + const button = document.querySelector(`.js-promote-project-milestone[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'); +promoteMilestoneButtons.forEach((button) => { + button.addEventListener('click', onDeleteButtonClick); +}); + +eventHub.$once('promoteMilestoneModal.mounted', () => { + promoteMilestoneButtons.forEach((button) => { + button.removeAttribute('disabled'); + }); +}); + +export default () => { + const promoteMilestoneComponent = new Vue({ + el: '#promote-milestone-modal', + 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, + }); + }, + }); + + const promoteMilestoneModal = document.getElementById('promote-milestone-modal'); + let withMilestone; + if (promoteMilestoneModal != null) { + withMilestone = promoteMilestoneComponent; + } + return withMilestone; +}; diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue new file mode 100644 index 00000000000..93fe5284d1b --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -0,0 +1,71 @@ + + diff --git a/app/assets/javascripts/pages/projects/labels/event_hub.js b/app/assets/javascripts/pages/projects/labels/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 6e45de2a724..32a8db7e989 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -1,3 +1,89 @@ +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 onRequestFinished = ({ labelUrl, successful }) => { + const button = document.querySelector(`.js-promote-project-label[data-url="${labelUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } +}; + +const onRequestStarted = (labelUrl) => { + const button = document.querySelector(`.js-promote-project-label[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, + url: button.dataset.url, + }; + eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted); + eventHub.$emit('promoteLabelModal.props', modalProps); +}; + +const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label'); +promoteLabelButtons.forEach((button) => { + button.addEventListener('click', onDeleteButtonClick); +}); + +eventHub.$once('promoteLabelModal.mounted', () => { + promoteLabelButtons.forEach((button) => { + button.removeAttribute('disabled'); + }); +}); + +const initLabelIndex = () => { + initLabels(); + + const promoteLabelModalComponent = new Vue({ + el: '#promote-label-modal', + components: { + PromoteLabelModal, + }, + data() { + return { + modalProps: { + labelTitle: '', + labelColor: '', + 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, + }); + }, + }); + + const promoteLabelModal = document.getElementById('promote-label-modal'); + let withLabel; + if (promoteLabelModal != null) { + withLabel = promoteLabelModalComponent; + } + return withLabel; +}; + +document.addEventListener('DOMContentLoaded', initLabelIndex); diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index a6b1bf9b099..25cdc66ec0d 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -2,7 +2,8 @@ background-color: $modal-body-bg; padding: #{3 * $grid-size} #{2 * $grid-size}; - .page-title { + .page-title, + .modal-title { margin-top: 0; .color-label { diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index e0f4710175f..439c11b0bf3 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -114,8 +114,7 @@ class Projects::LabelsController < Projects::ApplicationController 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.js end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 75b17d05e22..4fd36eb8cec 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -71,8 +71,7 @@ class Projects::MilestonesController < Projects::ApplicationController 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) + redirect_to group_milestone_path(project.group, promoted_milestone.iid), status: 303 rescue Milestones::PromoteService::PromoteMilestoneError => error redirect_to milestone, alert: error.message end diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 80e4dce1a80..9bbbc9d2758 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -3,6 +3,7 @@ - hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) +#promote-label-modal - if @labels.exists? || @prioritized_labels.exists? %div{ class: container_class } .top-area.adjust diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 6a7bc4b1888..5b0197ed58c 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -13,6 +13,7 @@ .milestones #delete-milestone-modal + #promote-milestone-modal %ul.content-list = render @milestones diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index de381d489c6..630a0583359 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -27,8 +27,14 @@ 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.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 } } + = _('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" diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 8847d11f623..a7b85ac4469 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -48,8 +48,14 @@ .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 + %a.js-promote-project-label.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), + data: { url: promote_project_label_path(label.project, label), + label_title: label.title, + label_color: label.color, + target: '#promote-label-modal', + container: 'body', + toggle: 'modal' }, + disabled: true } = 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 diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index e3b2b53833e..6245a4445a4 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -51,7 +51,13 @@ \ - 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 + %a.js-promote-project-milestone.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), + data: { url: promote_project_milestone_path(milestone.project, milestone), + milestone_title: milestone.title, + target: '#promote-milestone-modal', + container: 'body', + toggle: 'modal' }, + disabled: true } 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" diff --git a/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml b/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml new file mode 100644 index 00000000000..674bea0a764 --- /dev/null +++ b/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Added vue based promotion modals for labels and milestones +merge_request: 17197 +author: +type: other diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 00cf464ec5b..c7d98eef39f 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -101,7 +101,6 @@ describe Projects::MilestonesController do 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.') end end diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js new file mode 100644 index 00000000000..66b9ac648d1 --- /dev/null +++ b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js @@ -0,0 +1,99 @@ +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 * as urlUtility from '~/lib/utils/url_utility'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('Promote label modal', () => { + let vm; + let Component; + const labelMockData = { + labelTitle: 'Documentation', + labelColor: '#5cb85c', + url: `${gl.TEST_HOST}/dummy/endpoint`, + }; + + beforeEach(() => { + Component = Vue.extend(promoteLabelModal); + }); + + describe('Modal title and description', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...labelMockData, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should contain the proper description', () => { + expect(vm.text).toContain('Promoting this label will make it available for all projects inside the group'); + expect(vm.text).toContain('Existing project labels with the same name will be merged'); + expect(vm.text).toContain('This action cannot be reversed.'); + }); + + it('should contain 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('should redirect 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, + }, + }); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + }) + .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); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { labelUrl: labelMockData.url, successful: false }); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js new file mode 100644 index 00000000000..f201edddce0 --- /dev/null +++ b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js @@ -0,0 +1,95 @@ +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 * as urlUtility from '~/lib/utils/url_utility'; +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +describe('Promote milestone modal', () => { + let vm; + let Component; + const milestoneMockData = { + milestoneTitle: 'v1.0', + url: `${gl.TEST_HOST}/dummy/endpoint`, + }; + + beforeEach(() => { + Component = Vue.extend(promoteMilestoneModal); + }); + + describe('Modal title and description', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...milestoneMockData, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should contain the proper description', () => { + expect(vm.text).toContain('Promoting this milestone will make it available for all projects inside the group.'); + expect(vm.text).toContain('Existing project milestones with the same name will be merged.'); + expect(vm.text).toContain('This action cannot be reversed.'); + }); + + it('should contain 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('should redirect 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, + }, + }); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + }) + .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); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', { milestoneUrl: milestoneMockData.url, successful: false }); + }) + .then(done) + .catch(done.fail); + }); + }); +}); From 84e01b3e7a521c66077245a62247bd47a3731ddb Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Tue, 20 Feb 2018 13:43:37 -0600 Subject: [PATCH 2/4] Changed selector names, address code concerns --- .../components/promote_milestone_modal.vue | 10 +- .../shared/delete_milestone_modal_init.js | 10 +- .../shared/promote_milestone_modal_init.js | 138 ++++++++--------- .../labels/components/promote_label_modal.vue | 26 ++-- .../pages/projects/labels/index/index.js | 144 +++++++++--------- app/assets/stylesheets/framework/modal.scss | 6 +- app/views/projects/labels/index.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 6 +- app/views/shared/_label.html.haml | 3 +- .../shared/milestones/_milestone.html.haml | 4 +- ...vl-new-modal-project-labels-milestones.yml | 2 +- .../components/promote_label_modal_spec.js | 22 +-- .../promote_milestone_modal_spec.js | 21 +-- 13 files changed, 196 insertions(+), 198 deletions(-) diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index 4f9be4f4f99..eb49a5780bd 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -22,7 +22,7 @@ }, computed: { title() { - return sprintf(s__('Milestones|Promote %{title} to group milestone?'), { title: this.milestoneTitle }); + 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. @@ -35,11 +35,11 @@ eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); return axios.post(this.url) .then((response) => { - eventHub.$emit('promoteMilestoneModal.requestFinished', { labelUrl: this.url, successful: true }); + eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true }); redirectTo(response.request.responseURL); }) .catch((error) => { - eventHub.$emit('promoteMilestoneModal.requestFinished', { labelUrl: this.url, successful: true }); + eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false }); createFlash(error); }); }, @@ -53,11 +53,11 @@ :footer-primary-button-text="s__('Milestones|Promote Milestone')" @submit="onSubmit" > -
{{ title }} -
+ {{ text }} diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js index 326673f3a2f..d51b5c221e3 100644 --- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js @@ -37,16 +37,14 @@ export default () => { }; const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); - for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { - const button = deleteMilestoneButtons[i]; + deleteMilestoneButtons.forEach((button) => { button.addEventListener('click', onDeleteButtonClick); - } + }); eventHub.$once('deleteMilestoneModal.mounted', () => { - for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { - const button = deleteMilestoneButtons[i]; + deleteMilestoneButtons.forEach((button) => { button.removeAttribute('disabled'); - } + }); }); return new Vue({ diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js index ea9adb9fe76..d00f81c9094 100644 --- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js @@ -5,78 +5,78 @@ import eventHub from './event_hub'; Vue.use(Translate); -const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector(`.js-promote-project-milestone[data-url="${milestoneUrl}"]`); - - if (!successful) { - button.removeAttribute('disabled'); - } -}; - -const onRequestStarted = (milestoneUrl) => { - const button = document.querySelector(`.js-promote-project-milestone[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'); -promoteMilestoneButtons.forEach((button) => { - button.addEventListener('click', onDeleteButtonClick); -}); - -eventHub.$once('promoteMilestoneModal.mounted', () => { - promoteMilestoneButtons.forEach((button) => { - button.removeAttribute('disabled'); - }); -}); - export default () => { - const promoteMilestoneComponent = new Vue({ - el: '#promote-milestone-modal', - 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, - }); - }, + 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 withMilestone; - if (promoteMilestoneModal != null) { - withMilestone = promoteMilestoneComponent; + 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 withMilestone; + + return promoteMilestoneComponent; }; diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index 93fe5284d1b..a48784f08a4 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -3,7 +3,7 @@ import createFlash from '~/flash'; import GlModal from '~/vue_shared/components/gl_modal.vue'; import { redirectTo } from '~/lib/utils/url_utility'; - import { s__ } from '~/locale'; + import { s__, sprintf } from '~/locale'; import eventHub from '../event_hub'; export default { @@ -23,12 +23,26 @@ 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 name will be merged. This action cannot be reversed.`); }, + title() { + const label = `${this.labelTitle}`; + + return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), { + labelTitle: label, + }, false); + }, }, methods: { onSubmit() { @@ -55,15 +69,9 @@ >
- {{ s__('Labels|Promote label') }} - - {{ labelTitle }} - - {{ s__('Labels|to Group Label?') }} + {{ title }}
{{ text }} diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 32a8db7e989..2abcbfab1ed 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -6,84 +6,86 @@ import PromoteLabelModal from '../components/promote_label_modal.vue'; Vue.use(Translate); -const onRequestFinished = ({ labelUrl, successful }) => { - const button = document.querySelector(`.js-promote-project-label[data-url="${labelUrl}"]`); - - if (!successful) { - button.removeAttribute('disabled'); - } -}; - -const onRequestStarted = (labelUrl) => { - const button = document.querySelector(`.js-promote-project-label[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, - url: button.dataset.url, - }; - eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted); - eventHub.$emit('promoteLabelModal.props', modalProps); -}; - -const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label'); -promoteLabelButtons.forEach((button) => { - button.addEventListener('click', onDeleteButtonClick); -}); - -eventHub.$once('promoteLabelModal.mounted', () => { - promoteLabelButtons.forEach((button) => { - button.removeAttribute('disabled'); - }); -}); - const initLabelIndex = () => { initLabels(); - const promoteLabelModalComponent = new Vue({ - el: '#promote-label-modal', - components: { - PromoteLabelModal, - }, - data() { - return { - modalProps: { - labelTitle: '', - labelColor: '', - 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, - }); - }, + 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 withLabel; - if (promoteLabelModal != null) { - withLabel = promoteLabelModalComponent; + 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 withLabel; + + return promoteLabelModalComponent; }; document.addEventListener('DOMContentLoaded', initLabelIndex); diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 25cdc66ec0d..48b981dd31f 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -4,13 +4,15 @@ .page-title, .modal-title { - margin-top: 0; - .color-label { font-size: $gl-font-size; padding: $gl-vert-padding $label-padding-modal; } } + + .page-title { + margin-top: 0; + } } .modal-body { diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 9bbbc9d2758..9c78bade254 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -3,8 +3,8 @@ - hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) -#promote-label-modal - if @labels.exists? || @prioritized_labels.exists? + #promote-label-modal %div{ class: container_class } .top-area.adjust .nav-text diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 630a0583359..baf9dd465c0 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -27,12 +27,12 @@ Edit - if @project.group - %button.js-promote-project-milestone.btn.btn-grouped{ data: { toggle: 'modal', + %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 } } + container: 'body' }, + disabled: true } = _('Promote') #promote-milestone-modal diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index a7b85ac4469..b49be4a4376 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -48,10 +48,11 @@ .pull-right.hidden-xs.hidden-sm - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - %a.js-promote-project-label.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), + %a.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), 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' }, diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 6245a4445a4..7a23f79d8c5 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -51,14 +51,14 @@ \ - if @project.group - %a.js-promote-project-milestone.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), + %a.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), data: { url: promote_project_milestone_path(milestone.project, milestone), milestone_title: milestone.title, target: '#promote-milestone-modal', container: 'body', toggle: 'modal' }, disabled: true } - Promote + = _('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" diff --git a/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml b/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml index 674bea0a764..6b7e14c6cfc 100644 --- a/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml +++ b/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml @@ -1,5 +1,5 @@ --- -title: Added vue based promotion modals for labels and milestones +title: Added new design for promotion modals merge_request: 17197 author: type: other diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js index 66b9ac648d1..e68c957e135 100644 --- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js +++ b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js @@ -7,35 +7,28 @@ import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('Promote label modal', () => { let vm; - let Component; + const Component = Vue.extend(promoteLabelModal); const labelMockData = { labelTitle: 'Documentation', labelColor: '#5cb85c', - url: `${gl.TEST_HOST}/dummy/endpoint`, + labelTextColor: '#ffffff', + url: `${gl.TEST_HOST}/dummy/promote/labels`, }; - beforeEach(() => { - Component = Vue.extend(promoteLabelModal); - }); - describe('Modal title and description', () => { beforeEach(() => { - vm = mountComponent(Component, { - ...labelMockData, - }); + vm = mountComponent(Component, labelMockData); }); afterEach(() => { vm.$destroy(); }); - it('should contain the proper description', () => { + it('contains the proper description', () => { expect(vm.text).toContain('Promoting this label will make it available for all projects inside the group'); - expect(vm.text).toContain('Existing project labels with the same name will be merged'); - expect(vm.text).toContain('This action cannot be reversed.'); }); - it('should contain a label span with the color', () => { + 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); @@ -55,7 +48,7 @@ describe('Promote label modal', () => { vm.$destroy(); }); - it('should redirect when a label is promoted', (done) => { + 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); @@ -71,6 +64,7 @@ describe('Promote label modal', () => { vm.onSubmit() .then(() => { expect(redirectSpy).toHaveBeenCalledWith(responseURL); + expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { labelUrl: labelMockData.url, successful: true }); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js index f201edddce0..d0ec4df7232 100644 --- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js +++ b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js @@ -7,34 +7,26 @@ import mountComponent from '../../../../helpers/vue_mount_component_helper'; describe('Promote milestone modal', () => { let vm; - let Component; + const Component = Vue.extend(promoteMilestoneModal); const milestoneMockData = { milestoneTitle: 'v1.0', - url: `${gl.TEST_HOST}/dummy/endpoint`, + url: `${gl.TEST_HOST}/dummy/promote/milestones`, }; - beforeEach(() => { - Component = Vue.extend(promoteMilestoneModal); - }); - describe('Modal title and description', () => { beforeEach(() => { - vm = mountComponent(Component, { - ...milestoneMockData, - }); + vm = mountComponent(Component, milestoneMockData); }); afterEach(() => { vm.$destroy(); }); - it('should contain the proper description', () => { + it('contains the proper description', () => { expect(vm.text).toContain('Promoting this milestone will make it available for all projects inside the group.'); - expect(vm.text).toContain('Existing project milestones with the same name will be merged.'); - expect(vm.text).toContain('This action cannot be reversed.'); }); - it('should contain the correct title', () => { + it('contains the correct title', () => { expect(vm.title).toEqual('Promote v1.0 to group milestone?'); }); }); @@ -51,7 +43,7 @@ describe('Promote milestone modal', () => { vm.$destroy(); }); - it('should redirect when a milestone is promoted', (done) => { + 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); @@ -67,6 +59,7 @@ describe('Promote milestone modal', () => { vm.onSubmit() .then(() => { expect(redirectSpy).toHaveBeenCalledWith(responseURL); + expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', { milestoneUrl: milestoneMockData.url, successful: true }); }) .then(done) .catch(done.fail); From f42397d19a735de2e5ae38725270614826ec05da Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Tue, 27 Feb 2018 16:11:09 -0600 Subject: [PATCH 3/4] Add persistent flash messages --- .../javascripts/lib/utils/flash_queue.js | 46 +++++++++++++++++++ app/assets/javascripts/main.js | 2 + .../components/promote_milestone_modal.vue | 6 ++- .../labels/components/promote_label_modal.vue | 6 ++- app/controllers/projects/labels_controller.rb | 3 +- .../projects/milestones_controller.rb | 5 +- app/helpers/flash_helper.rb | 8 ++++ .../projects/labels_controller_spec.rb | 2 +- .../projects/milestones_controller_spec.rb | 4 +- 9 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/lib/utils/flash_queue.js create mode 100644 app/helpers/flash_helper.rb diff --git a/app/assets/javascripts/lib/utils/flash_queue.js b/app/assets/javascripts/lib/utils/flash_queue.js new file mode 100644 index 00000000000..f77afddbae3 --- /dev/null +++ b/app/assets/javascripts/lib/utils/flash_queue.js @@ -0,0 +1,46 @@ +import _ from 'underscore'; +import createFlash from '~/flash'; +import AccessorUtilities from '~/lib/utils/accessor'; + +const FLASH_QUEUE_KEY = 'flash-key'; + +export function popFlashMessage() { + const page = $('body').attr('data-page'); + let savedFlashMessages; + let returnVal = false; + + if (!page) { + return returnVal; + } + + if (AccessorUtilities.isLocalStorageAccessSafe()) { + savedFlashMessages = JSON.parse(window.localStorage.getItem(FLASH_QUEUE_KEY)); + const queuedMessage = _.findWhere(savedFlashMessages, { bodyData: page }); + if (queuedMessage) { + const queuedMessageIndex = _.findIndex(savedFlashMessages, { bodyData: page }); + createFlash(queuedMessage.message, queuedMessage.type); + savedFlashMessages.splice(queuedMessageIndex, 1); + window.localStorage.setItem(FLASH_QUEUE_KEY, JSON.stringify(savedFlashMessages)); + } + returnVal = true; + } + + return returnVal; +} + +export function saveFlashMessage(bodyData, message, type) { + let savedFlashMessages; + + if (AccessorUtilities.isLocalStorageAccessSafe()) { + savedFlashMessages = JSON.parse(window.localStorage.getItem(FLASH_QUEUE_KEY)); + if (!savedFlashMessages) { + savedFlashMessages = []; + } + savedFlashMessages.push({ + bodyData, + message, + type, + }); + window.localStorage.setItem(FLASH_QUEUE_KEY, JSON.stringify(savedFlashMessages)); + } +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 659dc9eaa1f..f76e2ae47a0 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -33,6 +33,7 @@ import './milestone_select'; import './projects_dropdown'; import './render_gfm'; import initBreadcrumbs from './breadcrumb'; +import { popFlashMessage } from './lib/utils/flash_queue'; import initDispatcher from './dispatcher'; @@ -262,4 +263,5 @@ document.addEventListener('DOMContentLoaded', () => { } initDispatcher(); + popFlashMessage(); }); diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index eb49a5780bd..af85c3915df 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -1,6 +1,7 @@