Commit aebffaae authored by Sean McGivern's avatar Sean McGivern

Merge branch 'feature/service-desk-integration' into 'feature/service-desk-be'

Service desk integration

See merge request !1519
parents 5194f415 a2e2837a
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
import Clipboard from 'vendor/clipboard';
import Clipboard from 'clipboard';
var genericError, genericSuccess, showTooltip;
......
......@@ -49,6 +49,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
const ShortcutsBlob = require('./shortcuts_blob');
......@@ -228,6 +229,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:edit':
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();
new NotificationsForm();
......
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
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>
<p class="settings-message">
We recommend you protect the external support email address.
Unblocked email spam would result in many spam issues being created,
and may disrupt your GitLab service.
</p>
</template>
</div>
`,
};
import Vue from 'vue';
export default new Vue();
/* 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 = 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.$on('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);
class ServiceDeskService {
constructor(endpoint) {
this.serviceDeskResource = Vue.resource(`${endpoint}`);
}
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;
});
}
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;
});
}
}
export default ServiceDeskService;
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 = value;
}
}
export default ServiceDeskStore;
.panel {
margin-bottom: $gl-padding;
@mixin panel {
.panel-heading {
padding: $gl-vert-padding $gl-padding;
line-height: 36px;
......@@ -48,3 +46,14 @@
line-height: inherit;
}
}
.panel {
@include panel;
margin-bottom: $gl-padding;
}
.panel-slim {
@extend .panel;
@include panel;
margin-bottom: $gl-vert-padding;
}
= render "projects/settings/head"
.project-edit-container
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
......@@ -125,6 +126,16 @@
= render 'merge_request_settings', form: f
- if EE::Gitlab::ServiceDesk.enabled?
%hr
%fieldset.js-service-desk-setting-wrapper.features.append-bottom-default
%h5.prepend-top-0
Service Desk
= link_to icon('question-circle'), help_page_path("TODO")
.js-service-desk-setting-root{ data: { endpoint: namespace_project_service_desk_path(@project.namespace, @project),
enabled: @project.service_desk_enabled,
incoming_email: (@project.service_desk_address if @project.service_desk_enabled) } }
%hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
......
require 'spec_helper'
describe 'Service Desk Setting', js: true, feature: true do
include WaitForAjax
let(:project) { create(:project_empty_repo, :private) }
let(:user) { create(:user) }
before do
project.add_master(user)
login_as(user)
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
visit edit_namespace_project_path(project.namespace, project)
end
it 'shows service desk activation checkbox' do
expect(page).to have_selector("#service-desk-enabled-checkbox")
end
it 'shows incoming email after activating' do
find("#service-desk-enabled-checkbox").click
wait_for_ajax
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';
const createComponent = (propsData) => {
const Component = Vue.extend(ServiceDeskSetting);
return new Component({
el: document.createElement('div'),
propsData,
});
};
describe('ServiceDeskSetting', () => {
let vm;
afterEach(() => {
if (vm) {
vm.$destroy();
}
});
describe('when isEnabled=true', () => {
let el;
describe('only isEnabled', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
});
el = vm.$el;
});
it('see main panel with the email info', () => {
expect(el.querySelector('.panel')).toBeDefined();
});
it('see loading spinner', () => {
expect(el.querySelector('.fa-spinner')).toBeDefined();
expect(el.querySelector('.fa-exclamation-circle')).toBeNull();
expect(vm.$refs['service-desk-incoming-email']).toBeUndefined();
});
it('see warning message', () => {
expect(el.querySelector('.settings-message')).toBeDefined();
});
});
describe('with incomingEmail', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
incomingEmail: 'foo@bar.com',
});
el = vm.$el;
});
it('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('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();
});
});
});
describe('when isEnabled=false', () => {
let el;
beforeEach(() => {
vm = createComponent({
isEnabled: false,
});
el = vm.$el;
});
it('should not see panel', () => {
expect(el.querySelector('.panel')).toBeNull();
});
it('should not see warning message', () => {
expect(el.querySelector('.settings-message')).toBeNull();
});
});
describe('methods', () => {
describe('onCheckboxToggle', () => {
let onCheckboxToggleSpy;
beforeEach(() => {
onCheckboxToggleSpy = jasmine.createSpy('spy');
eventHub.$on('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
vm = createComponent({
isEnabled: false,
});
});
afterEach(() => {
eventHub.$off('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
});
it('when getting checked', () => {
expect(onCheckboxToggleSpy).not.toHaveBeenCalled();
vm.onCheckboxToggle({
target: {
checked: true,
},
});
expect(onCheckboxToggleSpy).toHaveBeenCalledWith(true);
});
it('when getting unchecked', () => {
expect(onCheckboxToggleSpy).not.toHaveBeenCalled();
vm.onCheckboxToggle({
target: {
checked: false,
},
});
expect(onCheckboxToggleSpy).toHaveBeenCalledWith(false);
});
});
});
});
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}`);
});
});
});
});
import ServiceDeskStore from '~/projects/settings_service_desk/stores/service_desk_store';
describe('ServiceDeskStore', () => {
let store;
beforeEach(() => {
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('');
});
it('set true', () => {
const email = 'foo@bar.com';
store.setIncomingEmail(email);
expect(store.state.incomingEmail).toEqual(email);
});
});
describe('setFetchError', () => {
it('defaults to null', () => {
expect(store.state.fetchError).toEqual(null);
});
it('set true', () => {
const err = new Error('some-fake-failure');
store.setFetchError(err);
expect(store.state.fetchError).toEqual(err);
});
});
});
This diff is collapsed.
......@@ -1074,6 +1074,14 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
clipboard@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
dependencies:
good-listener "^1.2.0"
select "^1.1.2"
tiny-emitter "^1.0.0"
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
......@@ -1383,6 +1391,10 @@ delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
delegate@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe"
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
......@@ -2180,6 +2192,12 @@ globby@^5.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
good-listener@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
dependencies:
delegate "^3.1.2"
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
......@@ -3923,6 +3941,10 @@ select2@3.5.2-browserify:
version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
......@@ -4329,6 +4351,10 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
tiny-emitter@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
tmp@0.0.28, tmp@0.0.x:
version "0.0.28"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
......
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