Commit 20aeeabc authored by Eric Eastwood's avatar Eric Eastwood Committed by Sean McGivern

Service Desk Frontend

parent 5194f415
/* 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 ServiceDeskEntry from './projects/settings_service_desk/service_desk_entry';
const ShortcutsBlob = require('./shortcuts_blob');
......@@ -228,6 +229,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:edit':
const serviceDeskEntry = new ServiceDeskEntry(document.querySelector('.js-service-desk-setting-wrapper'));
serviceDeskEntry.init();
break;
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
......
import ClipboardAction from 'clipboard/lib/clipboard-action';
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskSetting',
props: {
isActivated: {
type: Boolean,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
fetchError: {
type: Object,
required: false,
default: null,
},
},
methods: {
onCheckboxToggle(e) {
const isChecked = e.target.checked;
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
},
copyIncomingEmail(e) {
e.preventDefault();
const clipboardAction = new ClipboardAction({
action: 'copy',
target: this.$refs['service-desk-incoming-email'],
text: this.incomingEmail,
trigger: e,
emitter: { emit: () => {} },
});
clipboardAction.destroy();
},
},
template: `
<div class="checkbox">
<label for="project_service_desk_enabled">
<input
type="checkbox"
value="1"
name="project[service_desk_enabled]"
id="project_service_desk_enabled"
:checked="isActivated"
@change="onCheckboxToggle($event)">
<span class="descr">
Activate service desk
</span>
</label>
<template v-if="isActivated">
<div
class="panel 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" />
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"
title="Copy incoming email address to clipboard"
@click="copyIncomingEmail($event)">
<i class="fa fa-clipboard" />
</button>
</template>
<template v-else>
<i class="fa fa-spinner fa-spin" />
</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 ServiceDeskEntry {
constructor(wrapperElement) {
this.wrapperElement = wrapperElement;
this.store = new ServiceDeskStore();
this.service = new ServiceDeskService('http://apilab.gitlap.com/some-project');
}
init() {
this.bindEvents();
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
:isActivated="isActivated"
:incomingEmail="incomingEmail"
:fetchError="fetchError" />
`,
components: {
'service-desk-setting': ServiceDeskSetting,
},
});
}
onEnableToggled(isChecked) {
this.store.setIsActivated(isChecked);
if (isChecked) {
this.store.setIncomingEmail('');
this.store.setFetchError(null);
this.service.fetchIncomingEmail()
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
}
destroy() {
this.unbindEvents();
if (this.vm) {
this.vm.$destroy();
}
}
}
export default ServiceDeskEntry;
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import vueResource from 'vue-resource';
import '../../../vue_shared/vue_resource_interceptor';
Vue.use(vueResource);
class ServiceDeskService {
constructor(endpointRoot) {
this.project = Vue.resource(`${endpointRoot}/fetch-incoming-service-desk-email`);
}
fetchIncomingEmail() {
return this.project.get()
.then(res => res.data.incomingEmail);
}
}
export default ServiceDeskService;
class ServiceDeskStore {
constructor(initialState = {}) {
this.state = Object.assign({
isActivated: false,
incomingEmail: '',
fetchError: null,
}, initialState);
}
setIsActivated(value) {
this.state.isActivated = value;
}
setIncomingEmail(value) {
this.state.incomingEmail = value;
}
setFetchError(value) {
this.state.fetchError = value;
}
}
export default ServiceDeskStore;
......@@ -71,6 +71,8 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
# TODO: remove
attr_accessor :service_desk_enabled
alias_attribute :title, :name
......
= render "projects/settings/head"
.project-edit-container
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
......@@ -125,6 +126,13 @@
= render 'merge_request_settings', form: f
%hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
Service Desk
= link_to icon('question-circle'), help_page_path("TODO")
.js-service-desk-setting-wrapper{ data: { enabled: @project.service_desk_enabled, incomingAddress: "TODO" } }
%hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
......
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 isActivated=true', () => {
let el;
describe('only isActivated', () => {
beforeEach(() => {
vm = createComponent({
isActivated: 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({
isActivated: 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({
isActivated: 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 isActivated=false', () => {
let el;
beforeEach(() => {
vm = createComponent({
isActivated: 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({
isActivated: 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);
});
});
describe('copyIncomingEmail', () => {
beforeEach(() => {
vm = createComponent({
isActivated: true,
incomingEmail: 'foo@bar.com',
});
});
it('copies text to clipboard');
});
});
});
import ServiceDeskService from '~/projects/settings_service_desk/services/service_desk_service';
describe('ServiceDeskService', () => {
let service;
beforeEach(() => {
service = new ServiceDeskService('');
});
it('fetchIncomingEmail', (done) => {
spyOn(service.project, 'get').and.returnValue(Promise.resolve({
data: {
incomingEmail: '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}`);
});
});
});
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.isActivated).toEqual(false);
});
it('set true', () => {
store.setIsActivated(true);
expect(store.state.isActivated).toEqual(true);
});
it('set false', () => {
store.setIsActivated(false);
expect(store.state.isActivated).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