Commit c60c6f16 authored by Phil Hughes's avatar Phil Hughes

Merge branch '2383-update-service-desk-to-new-styleguide-standards' into 'master'

Update Service Desk to use updated Vue standards (and remove from main bundle)

Closes #2383

See merge request !1874
parents dd23edce 7f3dd811
......@@ -53,7 +53,6 @@ import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown';
......@@ -265,11 +264,6 @@ import ApproversSelect from './approvers_select';
break;
case 'projects:edit':
new UsersSelect();
const el = document.querySelector('.js-service-desk-setting-root');
if (el) {
const serviceDeskRoot = new ServiceDeskRoot(el);
serviceDeskRoot.init();
}
break;
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
......
......@@ -16,7 +16,8 @@ window.Flash = (function() {
parent = null;
}
if (parent) {
this.flashContainer = parent.find('.flash-container');
const $parent = $(parent);
this.flashContainer = $parent.find('.flash-container');
} else {
this.flashContainer = $('.flash-container-page');
}
......@@ -37,5 +38,9 @@ window.Flash = (function() {
this.flashContainer.show();
}
Flash.prototype.destroy = function() {
this.flashContainer.html('');
};
return Flash;
})();
<script>
/* global Flash */
import serviceDeskSetting from './service_desk_setting.vue';
import ServiceDeskStore from '../stores/service_desk_store';
import ServiceDeskService from '../services/service_desk_service';
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskRoot',
props: {
initialIsEnabled: {
type: Boolean,
required: true,
},
endpoint: {
type: String,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
},
data() {
const store = new ServiceDeskStore({
incomingEmail: this.incomingEmail,
});
return {
store,
state: store.state,
isEnabled: this.initialIsEnabled,
};
},
components: {
serviceDeskSetting,
},
methods: {
fetchIncomingEmail() {
if (this.flash) {
this.flash.destroy();
}
this.service.fetchIncomingEmail()
.then(res => res.json())
.then((data) => {
const email = data.service_desk_address;
if (!email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
this.store.setIncomingEmail(email);
})
.catch(() => {
this.flash = new Flash('An error occurred while fetching the Service Desk address.', 'alert', this.$el);
});
},
onEnableToggled(isChecked) {
this.isEnabled = isChecked;
this.store.resetIncomingEmail();
if (this.flash) {
this.flash.destroy();
}
this.service.toggleServiceDesk(isChecked)
.then(res => res.json())
.then((data) => {
const email = data.service_desk_address;
if (isChecked && !email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
this.store.setIncomingEmail(email);
})
.catch(() => {
const verb = isChecked ? 'enabling' : 'disabling';
this.flash = new Flash(`An error occurred while ${verb} Service Desk.`, 'alert', this.$el);
});
},
},
created() {
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
this.service = new ServiceDeskService(this.endpoint);
if (this.isEnabled && !this.store.state.incomingEmail) {
this.fetchIncomingEmail();
}
},
beforeDestroy() {
eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
},
};
</script>
<template>
<div>
<div class="flash-container"></div>
<service-desk-setting
:is-enabled="isEnabled"
:incoming-email="state.incomingEmail" />
</div>
</template>
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskSetting',
props: {
isEnabled: {
type: Boolean,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
fetchError: {
type: Error,
required: false,
default: null,
},
},
methods: {
onCheckboxToggle(e) {
const isChecked = e.target.checked;
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
},
},
template: `
<div>
<div class="checkbox">
<label for="service-desk-enabled-checkbox">
<input
ref="enabled-checkbox"
type="checkbox"
id="service-desk-enabled-checkbox"
:checked="isEnabled"
@change="onCheckboxToggle($event)">
<span class="descr">
Activate Service Desk
</span>
</label>
</div>
<template v-if="isEnabled">
<div
class="panel-slim panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Forward external support email address to:
</h3>
</div>
<div class="panel-body">
<template v-if="fetchError">
<i class="fa fa-exclamation-circle" aria-hidden="true" />
An error occurred while fetching the incoming email
</template>
<template v-else-if="incomingEmail">
<span
ref="service-desk-incoming-email">
{{ incomingEmail }}
</span>
<button
class="btn btn-clipboard btn-transparent has-tooltip"
title="Copy incoming email address to clipboard"
:data-clipboard-text="incomingEmail"
@click.prevent>
<i class="fa fa-clipboard" aria-hidden="true" />
</button>
</template>
<template v-else>
<i class="fa fa-spinner fa-spin" aria-hidden="true" />
<span class="sr-only">
Fetching incoming email
</span>
</template>
</div>
</div>
</template>
</div>
`,
};
<script>
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskSetting',
props: {
isEnabled: {
type: Boolean,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
},
methods: {
onCheckboxToggle(e) {
const isChecked = e.target.checked;
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
},
},
};
</script>
<template>
<div>
<div class="checkbox">
<label for="service-desk-enabled-checkbox">
<input
ref="enabled-checkbox"
type="checkbox"
id="service-desk-enabled-checkbox"
:checked="isEnabled"
@change="onCheckboxToggle($event)">
<span class="descr">
Activate Service Desk
</span>
</label>
</div>
<div
v-if="isEnabled"
class="panel-slim panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Forward external support email address to:
</h3>
</div>
<div class="panel-body">
<template v-if="incomingEmail">
<span
ref="service-desk-incoming-email">
{{ incomingEmail }}
</span>
<button
type="button"
class="btn btn-clipboard btn-transparent has-tooltip"
title="Copy incoming email address to clipboard"
:data-clipboard-text="incomingEmail">
<i
class="fa fa-clipboard"
aria-hidden="true" />
</button>
</template>
<template v-else>
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
<span class="sr-only">
Fetching incoming email
</span>
</template>
</div>
</div>
</div>
</template>
import Vue from 'vue';
import serviceDeskRoot from './components/service_desk_root.vue';
document.addEventListener('DOMContentLoaded', () => {
const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root');
if (serviceDeskRootElement) {
// eslint-disable-next-line no-new
new Vue({
el: serviceDeskRootElement,
data() {
const dataset = serviceDeskRootElement.dataset;
return {
initialIsEnabled: gl.utils.convertPermissionToBoolean(
dataset.enabled,
),
endpoint: dataset.endpoint,
incomingEmail: dataset.incomingEmail,
};
},
components: {
serviceDeskRoot,
},
render(createElement) {
return createElement('service-desk-root', {
props: {
initialIsEnabled: this.initialIsEnabled,
endpoint: this.endpoint,
incomingEmail: this.incomingEmail,
},
});
},
});
}
});
/* eslint-disable no-new */
import Vue from 'vue';
import ServiceDeskSetting from './components/service_desk_setting';
import ServiceDeskStore from './stores/service_desk_store';
import ServiceDeskService from './services/service_desk_service';
import eventHub from './event_hub';
class ServiceDeskRoot {
constructor(wrapperElement) {
this.wrapperElement = wrapperElement;
const isEnabled = typeof this.wrapperElement.dataset.enabled !== 'undefined' &&
this.wrapperElement.dataset.enabled !== 'false';
const incomingEmail = this.wrapperElement.dataset.incomingEmail;
const endpoint = this.wrapperElement.dataset.endpoint;
this.store = new ServiceDeskStore({
isEnabled,
incomingEmail,
});
this.service = new ServiceDeskService(endpoint);
}
init() {
this.bindEvents();
if (this.store.state.isEnabled && !this.store.state.incomingEmail) {
this.fetchIncomingEmail();
}
this.render();
}
bindEvents() {
this.onEnableToggledWrapper = this.onEnableToggled.bind(this);
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggledWrapper);
}
unbindEvents() {
eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggledWrapper);
}
render() {
this.vm = new Vue({
el: this.wrapperElement,
data: this.store.state,
template: `
<service-desk-setting
:isEnabled="isEnabled"
:incomingEmail="incomingEmail"
:fetchError="fetchError" />
`,
components: {
'service-desk-setting': ServiceDeskSetting,
},
});
}
fetchIncomingEmail() {
this.service.fetchIncomingEmail()
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
onEnableToggled(isChecked) {
this.store.setIsActivated(isChecked);
this.store.setIncomingEmail('');
this.store.setFetchError(null);
this.service.toggleServiceDesk(isChecked)
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
destroy() {
this.unbindEvents();
if (this.vm) {
this.vm.$destroy();
}
}
}
export default ServiceDeskRoot;
import Vue from 'vue';
import vueResource from 'vue-resource';
import '../../../vue_shared/vue_resource_interceptor';
Vue.use(vueResource);
......@@ -10,28 +9,12 @@ class ServiceDeskService {
}
fetchIncomingEmail() {
return this.serviceDeskResource.get()
.then((res) => {
const email = res.data.service_desk_address;
if (!email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return email;
});
return this.serviceDeskResource.get();
}
toggleServiceDesk(enable) {
return this.serviceDeskResource.update({
service_desk_enabled: enable,
})
.then((res) => {
const email = res.data.service_desk_address;
if (enable && !email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return email;
});
}
}
......
class ServiceDeskStore {
constructor(initialState = {}) {
this.state = Object.assign({
isEnabled: false,
incomingEmail: '',
fetchError: null,
}, initialState);
}
setIsActivated(value) {
this.state.isEnabled = value;
}
setIncomingEmail(value) {
this.state.incomingEmail = value;
}
setFetchError(value) {
this.state.fetchError = new Error(value);
resetIncomingEmail() {
this.state.incomingEmail = '';
}
}
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('service_desk')
= render "projects/settings/head"
.project-edit-container
......@@ -133,7 +137,7 @@
Service Desk
= link_to icon('question-circle'), help_page_path('user/project/service_desk')
.js-service-desk-setting-root{ data: { endpoint: namespace_project_service_desk_path(@project.namespace, @project),
enabled: @project.service_desk_enabled,
enabled: "#{@project.service_desk_enabled}",
incoming_email: (@project.service_desk_address if @project.service_desk_enabled) } }
%hr
......
......@@ -54,6 +54,7 @@ var config = {
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
protected_tags: './protected_tags',
service_desk: './projects/settings_service_desk/service_desk_bundle.js',
sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
......@@ -151,6 +152,7 @@ var config = {
'pipelines_graph',
'schedule_form',
'schedules_index',
'service_desk',
'sidebar',
'vue_merge_request_widget',
],
......
......@@ -22,6 +22,9 @@ describe 'Service Desk Setting', js: true, feature: true do
it 'shows incoming email after activating' do
find("#service-desk-enabled-checkbox").click
wait_for_requests
project.reload
expect(project.service_desk_enabled).to be_truthy
expect(project.service_desk_address).to be_present
expect(find('.js-service-desk-setting-wrapper .panel-body')).to have_content(project.service_desk_address)
end
end
import Vue from 'vue';
import eventHub from '~/projects/settings_service_desk/event_hub';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting';
import serviceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
const createComponent = (propsData) => {
const Component = Vue.extend(ServiceDeskSetting);
describe('ServiceDeskSetting', () => {
let ServiceDeskSetting;
let vm;
return new Component({
el: document.createElement('div'),
propsData,
beforeEach(() => {
ServiceDeskSetting = Vue.extend(serviceDeskSetting);
});
};
describe('ServiceDeskSetting', () => {
let vm;
afterEach(() => {
if (vm) {
vm.$destroy();
......@@ -20,15 +17,14 @@ describe('ServiceDeskSetting', () => {
});
describe('when isEnabled=true', () => {
let el;
describe('only isEnabled', () => {
describe('as project admin', () => {
beforeEach(() => {
vm = createComponent({
vm = new ServiceDeskSetting({
propsData: {
isEnabled: true,
});
el = vm.$el;
},
}).$mount();
});
it('should see activation checkbox (not disabled)', () => {
......@@ -36,12 +32,12 @@ describe('ServiceDeskSetting', () => {
});
it('should see main panel with the email info', () => {
expect(el.querySelector('.panel')).toBeDefined();
expect(vm.$el.querySelector('.panel')).toBeDefined();
});
it('should see loading spinner', () => {
expect(el.querySelector('.fa-spinner')).toBeDefined();
expect(el.querySelector('.fa-exclamation-circle')).toBeNull();
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
expect(vm.$el.querySelector('.fa-exclamation-circle')).toBeNull();
expect(vm.$refs['service-desk-incoming-email']).toBeUndefined();
});
});
......@@ -49,50 +45,33 @@ describe('ServiceDeskSetting', () => {
describe('with incomingEmail', () => {
beforeEach(() => {
vm = createComponent({
vm = new ServiceDeskSetting({
propsData: {
isEnabled: true,
incomingEmail: 'foo@bar.com',
});
el = vm.$el;
},
}).$mount();
});
it('should see email', () => {
expect(vm.$refs['service-desk-incoming-email'].textContent.trim()).toEqual('foo@bar.com');
expect(el.querySelector('.fa-spinner')).toBeNull();
expect(el.querySelector('.fa-exclamation-circle')).toBeNull();
});
});
describe('with fetchError', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
fetchError: new Error('some-fake-failure'),
});
el = vm.$el;
});
it('should see error message', () => {
expect(el.querySelector('.fa-exclamation-circle')).toBeDefined();
expect(el.querySelector('.panel-body').textContent.trim()).toEqual('An error occurred while fetching the incoming email');
expect(el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$refs['service-desk-incoming-email']).toBeUndefined();
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.fa-exclamation-circle')).toBeNull();
});
});
});
describe('when isEnabled=false', () => {
let el;
beforeEach(() => {
vm = createComponent({
vm = new ServiceDeskSetting({
propsData: {
isEnabled: false,
});
el = vm.$el;
},
}).$mount();
});
it('should not see panel', () => {
expect(el.querySelector('.panel')).toBeNull();
expect(vm.$el.querySelector('.panel')).toBeNull();
});
it('should not see warning message', () => {
......@@ -108,9 +87,11 @@ describe('ServiceDeskSetting', () => {
onCheckboxToggleSpy = jasmine.createSpy('spy');
eventHub.$on('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
vm = createComponent({
vm = new ServiceDeskSetting({
propsData: {
isEnabled: false,
});
},
}).$mount();
});
afterEach(() => {
......
import ServiceDeskService from '~/projects/settings_service_desk/services/service_desk_service';
describe('ServiceDeskService', () => {
let service;
beforeEach(() => {
service = new ServiceDeskService('');
});
it('fetchIncomingEmail', (done) => {
spyOn(service.serviceDeskResource, 'get').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: true,
service_desk_address: 'foo@bar.com',
},
}));
service.fetchIncomingEmail()
.then((incomingEmail) => {
expect(incomingEmail).toEqual('foo@bar.com');
done();
})
.catch((err) => {
done.fail(`Failed to fetch incoming email:\n${err}`);
});
});
describe('toggleServiceDesk', () => {
it('enable Service Desk', (done) => {
spyOn(service.serviceDeskResource, 'update').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: true,
service_desk_address: 'foo@bar.com',
},
}));
service.toggleServiceDesk(true)
.then((incomingEmail) => {
expect(incomingEmail).toEqual('foo@bar.com');
done();
})
.catch((err) => {
done.fail(`Failed to enable Service Desk and fetch incoming email:\n${err}`);
});
});
it('disable Service Desk', (done) => {
spyOn(service.serviceDeskResource, 'update').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: false,
service_desk_address: null,
},
}));
service.toggleServiceDesk(false)
.then((incomingEmail) => {
expect(incomingEmail).toEqual(null);
done();
})
.catch((err) => {
done.fail(`Failed to disable Service Desk and reset incoming email:\n${err}`);
});
});
});
});
......@@ -7,24 +7,6 @@ describe('ServiceDeskStore', () => {
store = new ServiceDeskStore();
});
describe('setIsActivated', () => {
it('defaults to false', () => {
expect(store.state.isEnabled).toEqual(false);
});
it('set true', () => {
store.setIsActivated(true);
expect(store.state.isEnabled).toEqual(true);
});
it('set false', () => {
store.setIsActivated(false);
expect(store.state.isEnabled).toEqual(false);
});
});
describe('setIncomingEmail', () => {
it('defaults to an empty string', () => {
expect(store.state.incomingEmail).toEqual('');
......@@ -38,16 +20,12 @@ describe('ServiceDeskStore', () => {
});
});
describe('setFetchError', () => {
it('defaults to null', () => {
expect(store.state.fetchError).toEqual(null);
});
describe('resetIncomingEmail', () => {
it('resets to empty string', () => {
store.setIncomingEmail('foo');
store.resetIncomingEmail();
it('set true', () => {
const errMsg = 'some-fake-failure';
store.setFetchError(errMsg);
expect(store.state.fetchError).toEqual(new Error(errMsg));
expect(store.state.incomingEmail).toEqual('');
});
});
});
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