Commit 23e2b134 authored by Coung Ngo's avatar Coung Ngo

Add user ability to append template to incoming service desk issues

- Added dropdown to Service Desk section in repository settings to allow
  the user to select a template from `.gitlab/issue_templates` to append
  to all Service Desk issues
- Removed ServiceDeskStore to make Vue code more idiomatic
- Updated code to use gitlab-ui components
- Migrated from karma to jest
- Updated Service Desk documentation
parent 6a12ec81
...@@ -391,6 +391,10 @@ module IssuablesHelper ...@@ -391,6 +391,10 @@ module IssuablesHelper
end end
end end
def issuable_templates_names(issuable)
issuable_templates(issuable).map { |template| template[:name] }
end
def selected_template(issuable) def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] } params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end end
......
...@@ -57,7 +57,7 @@ you can skip the step 1 below; you only need to enable it per project. ...@@ -57,7 +57,7 @@ you can skip the step 1 below; you only need to enable it per project.
support [email sub-addressing](../../administration/incoming_email.md#email-sub-addressing). support [email sub-addressing](../../administration/incoming_email.md#email-sub-addressing).
1. Navigate to your project's **Settings** and scroll down to the **Service Desk** 1. Navigate to your project's **Settings** and scroll down to the **Service Desk**
section. section.
1. If you have the correct access and an Premium license, 1. If you have the correct access and a Premium license,
you will see an option to set up Service Desk: you will see an option to set up Service Desk:
![Activate Service Desk option](img/service_desk_disabled.png) ![Activate Service Desk option](img/service_desk_disabled.png)
...@@ -79,6 +79,9 @@ you can skip the step 1 below; you only need to enable it per project. ...@@ -79,6 +79,9 @@ you can skip the step 1 below; you only need to enable it per project.
However the older format is still supported, allowing existing aliases However the older format is still supported, allowing existing aliases
or contacts to continue working._ or contacts to continue working._
1. If you have [templates](description_templates.md) in your repository, then you can optionally
select one of these templates from the dropdown to append it to all Service Desk issues.
1. Service Desk is now enabled for this project! You should be able to access it from your project's navigation **Issue submenu**: 1. Service Desk is now enabled for this project! You should be able to access it from your project's navigation **Issue submenu**:
![Service Desk Navigation Item](img/service_desk_nav_item.png) ![Service Desk Navigation Item](img/service_desk_nav_item.png)
......
<script> <script>
import Flash from '~/flash'; import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import serviceDeskSetting from './service_desk_setting.vue'; import ServiceDeskSetting from './service_desk_setting.vue';
import ServiceDeskStore from '../stores/service_desk_store';
import ServiceDeskService from '../services/service_desk_service'; import ServiceDeskService from '../services/service_desk_service';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
name: 'ServiceDeskRoot', name: 'ServiceDeskRoot',
components: { components: {
serviceDeskSetting, GlAlert,
ServiceDeskSetting,
}, },
props: { props: {
initialIsEnabled: { initialIsEnabled: {
...@@ -21,45 +20,52 @@ export default { ...@@ -21,45 +20,52 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
incomingEmail: { initialIncomingEmail: {
type: String,
required: false,
default: '',
},
selectedTemplate: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
templates: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
const store = new ServiceDeskStore({
incomingEmail: this.incomingEmail,
});
return { return {
store,
state: store.state,
isEnabled: this.initialIsEnabled, isEnabled: this.initialIsEnabled,
incomingEmail: this.initialIncomingEmail,
isTemplateSaving: false,
isAlertShowing: false,
alertVariant: 'danger',
alertMessage: '',
}; };
}, },
created() { created() {
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled); eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
eventHub.$on('serviceDeskTemplateSave', this.onSaveTemplate);
this.service = new ServiceDeskService(this.endpoint); this.service = new ServiceDeskService(this.endpoint);
if (this.isEnabled && !this.store.state.incomingEmail) { if (this.isEnabled && !this.incomingEmail) {
this.fetchIncomingEmail(); this.fetchIncomingEmail();
} }
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled); eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
eventHub.$off('serviceDeskTemplateSave', this.onSaveTemplate);
}, },
methods: { methods: {
fetchIncomingEmail() { fetchIncomingEmail() {
if (this.flash) {
this.flash.innerHTML = '';
}
this.service this.service
.fetchIncomingEmail() .fetchIncomingEmail()
.then(({ data }) => { .then(({ data }) => {
...@@ -68,24 +74,16 @@ export default { ...@@ -68,24 +74,16 @@ export default {
throw new Error(__("Response didn't include `service_desk_address`")); throw new Error(__("Response didn't include `service_desk_address`"));
} }
this.store.setIncomingEmail(email); this.incomingEmail = email;
}) })
.catch(() => { .catch(() =>
this.flash = Flash( this.showAlert(__('An error occurred while fetching the Service Desk address.')),
__('An error occurred while fetching the Service Desk address.'),
'alert',
this.$el,
); );
});
}, },
onEnableToggled(isChecked) { onEnableToggled(isChecked) {
this.isEnabled = isChecked; this.isEnabled = isChecked;
this.store.resetIncomingEmail(); this.incomingEmail = '';
if (this.flash) {
this.flash.remove();
this.flash = undefined;
}
this.service this.service
.toggleServiceDesk(isChecked) .toggleServiceDesk(isChecked)
...@@ -95,23 +93,56 @@ export default { ...@@ -95,23 +93,56 @@ export default {
throw new Error(__("Response didn't include `service_desk_address`")); throw new Error(__("Response didn't include `service_desk_address`"));
} }
this.store.setIncomingEmail(email); this.incomingEmail = email;
}) })
.catch(() => { .catch(() => {
const message = isChecked const message = isChecked
? __('An error occurred while enabling Service Desk.') ? __('An error occurred while enabling Service Desk.')
: __('An error occurred while disabling Service Desk.'); : __('An error occurred while disabling Service Desk.');
this.flash = Flash(message, 'alert', this.$el); this.showAlert(message);
}); });
}, },
onSaveTemplate(template) {
this.isTemplateSaving = true;
this.service
.updateTemplate(template, this.isEnabled)
.then(() => this.showAlert(__('Template was successfully saved.'), 'success'))
.catch(() =>
this.showAlert(
__('An error occurred while saving the template. Please check if the template exists.'),
),
)
.finally(() => {
this.isTemplateSaving = false;
});
},
showAlert(message, variant = 'danger') {
this.isAlertShowing = true;
this.alertMessage = message;
this.alertVariant = variant;
},
onDismiss() {
this.isAlertShowing = false;
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<div class="flash-container"></div> <gl-alert v-if="isAlertShowing" class="mb-3" :variant="alertVariant" @dismiss="onDismiss">
<service-desk-setting :is-enabled="isEnabled" :incoming-email="state.incomingEmail" /> {{ alertMessage }}
</gl-alert>
<service-desk-setting
:is-enabled="isEnabled"
:incoming-email="incomingEmail"
:initial-selected-template="selectedTemplate"
:templates="templates"
:is-template-saving="isTemplateSaving"
/>
</div> </div>
</template> </template>
<script> <script>
import Toggle from '~/vue_shared/components/toggle_button.vue'; import { GlButton, GlFormSelect, GlToggle } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -9,12 +9,12 @@ export default { ...@@ -9,12 +9,12 @@ export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
ClipboardButton, ClipboardButton,
Toggle, GlButton,
GlFormSelect,
GlToggle,
}, },
props: { props: {
isEnabled: { isEnabled: {
type: Boolean, type: Boolean,
...@@ -25,25 +25,54 @@ export default { ...@@ -25,25 +25,54 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
initialSelectedTemplate: {
type: String,
required: false,
default: '',
},
templates: {
type: Array,
required: false,
default: () => [],
},
isTemplateSaving: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
selectedTemplate: this.initialSelectedTemplate,
};
},
computed: {
templateOptions() {
return [''].concat(this.templates);
},
}, },
methods: { methods: {
onCheckboxToggle(isChecked) { onCheckboxToggle(isChecked) {
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked); eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
}, },
onSaveTemplate() {
eventHub.$emit('serviceDeskTemplateSave', this.selectedTemplate);
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<toggle <gl-toggle
id="service-desk-checkbox" id="service-desk-checkbox"
ref="service-desk-checkbox"
:value="isEnabled" :value="isEnabled"
class="d-inline-block align-middle mr-1" class="d-inline-block align-middle mr-1"
:label-on="__('Service Desk is on')"
:label-off="__('Service Desk is off')"
@change="onCheckboxToggle" @change="onCheckboxToggle"
/> />
<label class="font-weight-bold" for="service-desk-checkbox"> <label class="align-middle" for="service-desk-checkbox">
{{ __('Activate Service Desk') }} {{ __('Activate Service Desk') }}
</label> </label>
<div v-if="isEnabled" class="row mt-3"> <div v-if="isEnabled" class="row mt-3">
...@@ -76,6 +105,19 @@ export default { ...@@ -76,6 +105,19 @@ export default {
<i class="fa fa-spinner fa-spin" aria-hidden="true"> </i> <i class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
<span class="sr-only">{{ __('Fetching incoming email') }}</span> <span class="sr-only">{{ __('Fetching incoming email') }}</span>
</template> </template>
<label for="service-desk-template-select" class="mt-3">
{{ __('Template to append to all Service Desk issues') }}
</label>
<gl-form-select
id="service-desk-template-select"
v-model="selectedTemplate"
class="mb-3"
:options="templateOptions"
/>
<gl-button variant="success" :disabled="isTemplateSaving" @click="onSaveTemplate">
{{ __('Save template') }}
</gl-button>
</div> </div>
</div> </div>
</div> </div>
......
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import serviceDeskRoot from './components/service_desk_root.vue'; import ServiceDeskRoot from './components/service_desk_root.vue';
export default () => { export default () => {
const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root'); const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root');
...@@ -9,7 +9,7 @@ export default () => { ...@@ -9,7 +9,7 @@ export default () => {
new Vue({ new Vue({
el: serviceDeskRootElement, el: serviceDeskRootElement,
components: { components: {
serviceDeskRoot, ServiceDeskRoot,
}, },
data() { data() {
const { dataset } = serviceDeskRootElement; const { dataset } = serviceDeskRootElement;
...@@ -17,6 +17,8 @@ export default () => { ...@@ -17,6 +17,8 @@ export default () => {
initialIsEnabled: parseBoolean(dataset.enabled), initialIsEnabled: parseBoolean(dataset.enabled),
endpoint: dataset.endpoint, endpoint: dataset.endpoint,
incomingEmail: dataset.incomingEmail, incomingEmail: dataset.incomingEmail,
selectedTemplate: dataset.selectedTemplate,
templates: JSON.parse(dataset.templates),
}; };
}, },
render(createElement) { render(createElement) {
...@@ -24,7 +26,9 @@ export default () => { ...@@ -24,7 +26,9 @@ export default () => {
props: { props: {
initialIsEnabled: this.initialIsEnabled, initialIsEnabled: this.initialIsEnabled,
endpoint: this.endpoint, endpoint: this.endpoint,
incomingEmail: this.incomingEmail, initialIncomingEmail: this.incomingEmail,
selectedTemplate: this.selectedTemplate,
templates: this.templates,
}, },
}); });
}, },
......
...@@ -12,6 +12,14 @@ class ServiceDeskService { ...@@ -12,6 +12,14 @@ class ServiceDeskService {
toggleServiceDesk(enable) { toggleServiceDesk(enable) {
return axios.put(this.endpoint, { service_desk_enabled: enable }); return axios.put(this.endpoint, { service_desk_enabled: enable });
} }
updateTemplate(template, isEnabled) {
const body = {
issue_template_key: template,
service_desk_enabled: isEnabled,
};
return axios.put(this.endpoint, body);
}
} }
export default ServiceDeskService; export default ServiceDeskService;
class ServiceDeskStore {
constructor(initialState = {}) {
this.state = Object.assign(
{
incomingEmail: '',
},
initialState,
);
}
setIncomingEmail(value) {
this.state.incomingEmail = value;
}
resetIncomingEmail() {
this.state.incomingEmail = '';
}
}
export default ServiceDeskStore;
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
- if EE::Gitlab::ServiceDesk.enabled?(project: @project) - if EE::Gitlab::ServiceDesk.enabled?(project: @project)
.js-service-desk-setting-root{ data: { endpoint: project_service_desk_path(@project), .js-service-desk-setting-root{ data: { endpoint: project_service_desk_path(@project),
enabled: "#{@project.service_desk_enabled}", enabled: "#{@project.service_desk_enabled}",
incoming_email: (@project.service_desk_address if @project.service_desk_enabled) } } incoming_email: (@project.service_desk_address if @project.service_desk_enabled),
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
templates: issuable_templates_names(Issue.new) } }
- elsif show_promotions? && show_callout?('promote_service_desk_dismissed') - elsif show_promotions? && show_callout?('promote_service_desk_dismissed')
= render 'shared/promotions/promote_servicedesk' = render 'shared/promotions/promote_servicedesk'
---
title: Add user ability to append template to incoming service desk issues
merge_request: 20476
author:
type: added
import { shallowMount, mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import ServiceDeskRoot from 'ee/projects/settings_service_desk/components/service_desk_root.vue';
describe('ServiceDeskRoot', () => {
const endpoint = '/gitlab-org/gitlab-test/service_desk';
const initialIncomingEmail = 'servicedeskaddress@example.com';
let axiosMock;
let wrapper;
let spy;
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
axiosMock.restore();
wrapper.destroy();
if (spy) {
spy.mockRestore();
}
});
it('fetches incoming email when there is no incoming email provided', () => {
axiosMock.onGet(endpoint).replyOnce(httpStatusCodes.OK);
wrapper = shallowMount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
initialIncomingEmail: '',
endpoint,
},
});
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(axiosMock.history.get).toHaveLength(1);
});
});
it('does not fetch incoming email when there is an incoming email provided', () => {
axiosMock.onGet(endpoint).replyOnce(httpStatusCodes.OK);
wrapper = shallowMount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
initialIncomingEmail,
endpoint,
},
});
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(axiosMock.history.get).toHaveLength(0);
});
});
it('shows an error message when incoming email is not fetched correctly', () => {
axiosMock.onGet(endpoint).networkError();
wrapper = shallowMount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
initialIncomingEmail: '',
endpoint,
},
});
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.html()).toContain(
'An error occurred while fetching the Service Desk address.',
);
});
});
it('sends a request to toggle service desk off when the toggle is clicked from the on state', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
spy = jest.spyOn(axios, 'put');
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
initialIncomingEmail,
endpoint,
},
});
wrapper.find('button.gl-toggle').trigger('click');
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(spy).toHaveBeenCalledWith(endpoint, { service_desk_enabled: false });
});
});
it('sends a request to toggle service desk on when the toggle is clicked from the off state', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
spy = jest.spyOn(axios, 'put');
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: false,
initialIncomingEmail: '',
endpoint,
},
});
wrapper.find('button.gl-toggle').trigger('click');
return wrapper.vm.$nextTick(() => {
expect(spy).toHaveBeenCalledWith(endpoint, { service_desk_enabled: true });
});
});
it('shows an error message when there is an issue toggling service desk on', () => {
axiosMock.onPut(endpoint).networkError();
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: false,
initialIncomingEmail: '',
endpoint,
},
});
wrapper.find('button.gl-toggle').trigger('click');
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.html()).toContain('An error occurred while enabling Service Desk.');
});
});
it('sends a request to update template when the "Save template" button is clicked', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
spy = jest.spyOn(axios, 'put');
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
endpoint,
initialIncomingEmail,
selectedTemplate: 'Bug',
templates: ['Bug', 'Documentation'],
},
sync: false,
});
wrapper.find('button.btn-success').trigger('click');
return wrapper.vm.$nextTick(() => {
expect(spy).toHaveBeenCalledWith(endpoint, {
issue_template_key: 'Bug',
service_desk_enabled: true,
});
});
});
it('saves the template when the "Save template" button is clicked', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
endpoint,
initialIncomingEmail,
selectedTemplate: 'Bug',
templates: ['Bug', 'Documentation'],
},
sync: false,
});
wrapper.find('button.btn-success').trigger('click');
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.html()).toContain('Template was successfully saved.');
});
});
it('shows an error message when there is an issue saving the template', () => {
axiosMock.onPut(endpoint).networkError();
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
endpoint,
initialIncomingEmail,
selectedTemplate: 'Bug',
templates: ['Bug', 'Documentation'],
},
sync: false,
});
wrapper.find('button.btn-success').trigger('click');
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.html()).toContain(
'An error occurred while saving the template. Please check if the template exists.',
);
});
});
});
import { shallowMount, mount } from '@vue/test-utils';
import eventHub from 'ee/projects/settings_service_desk/event_hub';
import ServiceDeskSetting from 'ee/projects/settings_service_desk/components/service_desk_setting.vue';
describe('ServiceDeskSetting', () => {
let wrapper;
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
const findTemplateDropdown = () => wrapper.find('#service-desk-template-select');
describe('when isEnabled=true', () => {
describe('only isEnabled', () => {
describe('as project admin', () => {
beforeEach(() => {
wrapper = shallowMount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
},
});
});
it('should see activation checkbox', () => {
expect(wrapper.contains('#service-desk-checkbox')).toBe(true);
});
it('should see main panel with the email info', () => {
expect(wrapper.contains('#incoming-email-describer')).toBe(true);
});
it('should see loading spinner and not the incoming email', () => {
expect(wrapper.contains('.fa-spinner')).toBe(true);
expect(wrapper.contains('.incoming-email')).toBe(false);
});
});
});
describe('service desk toggle', () => {
it('emits an event to turn on Service Desk when clicked', () => {
const eventSpy = jest.fn();
eventHub.$on('serviceDeskEnabledCheckboxToggled', eventSpy);
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: false,
},
});
wrapper.find('#service-desk-checkbox').trigger('click');
expect(eventSpy).toHaveBeenCalledWith(true);
eventHub.$off('serviceDeskEnabledCheckboxToggled', eventSpy);
eventSpy.mockRestore();
});
});
describe('with incomingEmail', () => {
const incomingEmail = 'foo@bar.com';
beforeEach(() => {
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
incomingEmail,
},
});
});
it('should see email and not the loading spinner', () => {
expect(wrapper.find('.incoming-email').element.value).toEqual(incomingEmail);
expect(wrapper.contains('.fa-spinner')).toBe(false);
});
it('renders a copy to clipboard button', () => {
expect(wrapper.contains('.qa-clipboard-button')).toBe(true);
expect(wrapper.find('.qa-clipboard-button').element.dataset.clipboardText).toBe(
incomingEmail,
);
});
});
describe('templates dropdown', () => {
it('renders a dropdown to choose a template', () => {
wrapper = shallowMount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
},
});
expect(wrapper.contains('#service-desk-template-select')).toBe(true);
});
it('renders a dropdown with a default value of ""', () => {
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
},
});
expect(findTemplateDropdown().element.value).toEqual('');
});
it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
const templates = ['Bug', 'Documentation', 'Security release'];
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
initialSelectedTemplate: 'Bug',
templates,
},
});
expect(findTemplateDropdown().element.value).toEqual('Bug');
});
it('renders a dropdown with no options when the project has no templates', () => {
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
templates: [],
},
});
// The dropdown by default has one empty option
expect(findTemplateDropdown().element.children.length).toEqual(1);
});
it('renders a dropdown with options when the project has templates', () => {
const templates = ['Bug', 'Documentation', 'Security release'];
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
templates,
},
});
// An empty-named template is prepended so the user can select no template
const expectedTemplates = [''].concat(templates);
const dropdown = findTemplateDropdown();
const dropdownList = Array.from(dropdown.element.children).map(option => option.innerText);
expect(dropdown.element.children.length).toEqual(expectedTemplates.length);
expect(dropdownList.includes('Bug')).toEqual(true);
expect(dropdownList.includes('Documentation')).toEqual(true);
expect(dropdownList.includes('Security release')).toEqual(true);
});
});
});
describe('save button', () => {
it('renders a save button to save a template', () => {
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
},
});
expect(wrapper.find('button.btn-success').text()).toContain('Save template');
});
it('emits a save event with the chosen template when the save button is clicked', () => {
const eventSpy = jest.fn();
eventHub.$on('serviceDeskTemplateSave', eventSpy);
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
initialSelectedTemplate: 'Bug',
},
});
wrapper.find('button.btn-success').trigger('click');
expect(eventSpy).toHaveBeenCalledWith('Bug');
eventHub.$off('serviceDeskTemplateSave', eventSpy);
eventSpy.mockRestore();
});
});
describe('when isEnabled=false', () => {
beforeEach(() => {
wrapper = shallowMount(ServiceDeskSetting, {
propsData: {
isEnabled: false,
},
});
});
it('does not render email panel', () => {
expect(wrapper.contains('#incoming-email-describer')).toBe(false);
});
it('does not render template dropdown', () => {
expect(wrapper.contains('#service-desk-template-select')).toBe(false);
});
it('does not render template save button', () => {
expect(wrapper.contains('button.btn-success')).toBe(false);
});
it('emits an event to turn on Service Desk when the toggle is clicked', () => {
const eventSpy = jest.fn();
eventHub.$on('serviceDeskEnabledCheckboxToggled', eventSpy);
wrapper = mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
},
});
wrapper.find('#service-desk-checkbox').trigger('click');
expect(eventSpy).toHaveBeenCalledWith(false);
eventHub.$off('serviceDeskEnabledCheckboxToggled', eventSpy);
eventSpy.mockRestore();
});
});
});
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import ServiceDeskService from 'ee/projects/settings_service_desk/services/service_desk_service';
describe('ServiceDeskService', () => {
const endpoint = `/gitlab-org/gitlab-test/service_desk`;
const dummyResponse = { message: 'Dummy response' };
const errorMessage = 'Network Error';
let axiosMock;
let service;
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
service = new ServiceDeskService(endpoint);
});
afterEach(() => {
axiosMock.restore();
});
describe('fetchIncomingEmail', () => {
it('makes a request to fetch incoming email', () => {
axiosMock.onGet(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
return service.fetchIncomingEmail().then(response => {
expect(response.data).toEqual(dummyResponse);
});
});
it('fails on error response', () => {
axiosMock.onGet(endpoint).networkError();
return service.fetchIncomingEmail().catch(error => {
expect(error.message).toBe(errorMessage);
});
});
});
describe('toggleServiceDesk', () => {
it('makes a request to set service desk', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
return service.toggleServiceDesk(true).then(response => {
expect(response.data).toEqual(dummyResponse);
});
});
it('fails on error response', () => {
axiosMock.onPut(endpoint).networkError();
return service.toggleServiceDesk(true).catch(error => {
expect(error.message).toBe(errorMessage);
});
});
it('makes a request with the expected body', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
const spy = jest.spyOn(axios, 'put');
service.toggleServiceDesk(true);
expect(spy).toHaveBeenCalledWith(endpoint, {
service_desk_enabled: true,
});
spy.mockRestore();
});
});
describe('updateTemplate', () => {
it('makes a request to update template', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
return service.updateTemplate('Bug', true).then(response => {
expect(response.data).toEqual(dummyResponse);
});
});
it('fails on error response', () => {
axiosMock.onPut(endpoint).networkError();
return service.updateTemplate('Bug', true).catch(error => {
expect(error.message).toBe(errorMessage);
});
});
it('makes a request with the expected body', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
const spy = jest.spyOn(axios, 'put');
service.updateTemplate('Bug', true);
expect(spy).toHaveBeenCalledWith(endpoint, {
issue_template_key: 'Bug',
service_desk_enabled: true,
});
spy.mockRestore();
});
});
});
import ServiceDeskStore from 'ee/projects/settings_service_desk/stores/service_desk_store';
describe('ServiceDeskStore', () => {
let store;
beforeEach(() => {
store = new ServiceDeskStore();
});
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('resetIncomingEmail', () => {
it('resets to empty string', () => {
store.setIncomingEmail('foo');
store.resetIncomingEmail();
expect(store.state.incomingEmail).toEqual('');
});
});
});
import Vue from 'vue';
import eventHub from 'ee/projects/settings_service_desk/event_hub';
import serviceDeskSetting from 'ee/projects/settings_service_desk/components/service_desk_setting.vue';
describe('ServiceDeskSetting', () => {
let ServiceDeskSetting;
let vm;
beforeEach(() => {
ServiceDeskSetting = Vue.extend(serviceDeskSetting);
});
afterEach(() => {
if (vm) {
vm.$destroy();
}
});
describe('when isEnabled=true', () => {
describe('only isEnabled', () => {
describe('as project admin', () => {
beforeEach(() => {
vm = new ServiceDeskSetting({
propsData: {
isEnabled: true,
},
}).$mount();
});
it('should see activation checkbox (not disabled)', () => {
const checkbox = vm.$refs['service-desk-checkbox'].$el;
expect(checkbox.querySelector('.project-feature-toggle:not(.is-checked)')).toEqual(null);
});
it('should see main panel with the email info', () => {
expect(vm.$el.querySelector('.card')).toBeDefined();
});
it('should see loading spinner', () => {
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
expect(vm.$el.querySelector('.fa-exclamation-circle')).toBeNull();
expect(vm.$refs['service-desk-incoming-email']).toBeUndefined();
});
});
});
describe('with incomingEmail', () => {
const incomingEmail = 'foo@bar.com';
beforeEach(() => {
vm = new ServiceDeskSetting({
propsData: {
isEnabled: true,
incomingEmail,
},
}).$mount();
});
it('should see email', () => {
expect(vm.$refs['service-desk-incoming-email'].value.trim()).toEqual(incomingEmail);
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.fa-exclamation-circle')).toBeNull();
});
it('renders a copy to clipboard button', () => {
const button = vm.$el.querySelector('.qa-clipboard-button');
expect(button).not.toBe(null);
expect(button.dataset.clipboardText).toBe(incomingEmail);
});
});
});
describe('when isEnabled=false', () => {
beforeEach(() => {
vm = new ServiceDeskSetting({
propsData: {
isEnabled: false,
},
}).$mount();
});
it('should not see panel', () => {
expect(vm.$el.querySelector('.card')).toBeNull();
});
it('should not see warning message', () => {
expect(vm.$refs['recommend-protect-email-from-spam-message']).toBeUndefined();
});
});
describe('methods', () => {
describe('onCheckboxToggle', () => {
let onCheckboxToggleSpy;
beforeEach(() => {
onCheckboxToggleSpy = jasmine.createSpy('spy');
eventHub.$on('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
vm = new ServiceDeskSetting({
propsData: {
isEnabled: false,
},
}).$mount();
});
afterEach(() => {
eventHub.$off('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
});
it('when getting checked', () => {
expect(onCheckboxToggleSpy).not.toHaveBeenCalled();
vm.onCheckboxToggle(true);
expect(onCheckboxToggleSpy).toHaveBeenCalledWith(true);
});
it('when getting unchecked', () => {
expect(onCheckboxToggleSpy).not.toHaveBeenCalled();
vm.onCheckboxToggle(false);
expect(onCheckboxToggleSpy).toHaveBeenCalledWith(false);
});
});
});
});
...@@ -1702,6 +1702,9 @@ msgstr "" ...@@ -1702,6 +1702,9 @@ msgstr ""
msgid "An error occurred while saving the approval settings" msgid "An error occurred while saving the approval settings"
msgstr "" msgstr ""
msgid "An error occurred while saving the template. Please check if the template exists."
msgstr ""
msgid "An error occurred while subscribing to notifications." msgid "An error occurred while subscribing to notifications."
msgstr "" msgstr ""
...@@ -15048,6 +15051,9 @@ msgstr "" ...@@ -15048,6 +15051,9 @@ msgstr ""
msgid "Save pipeline schedule" msgid "Save pipeline schedule"
msgstr "" msgstr ""
msgid "Save template"
msgstr ""
msgid "Save variables" msgid "Save variables"
msgstr "" msgstr ""
...@@ -15709,6 +15715,12 @@ msgstr "" ...@@ -15709,6 +15715,12 @@ msgstr ""
msgid "Service Desk is enabled but not yet active" msgid "Service Desk is enabled but not yet active"
msgstr "" msgstr ""
msgid "Service Desk is off"
msgstr ""
msgid "Service Desk is on"
msgstr ""
msgid "Service Templates" msgid "Service Templates"
msgstr "" msgstr ""
...@@ -17124,6 +17136,12 @@ msgstr "" ...@@ -17124,6 +17136,12 @@ msgstr ""
msgid "Template" msgid "Template"
msgstr "" msgstr ""
msgid "Template to append to all Service Desk issues"
msgstr ""
msgid "Template was successfully saved."
msgstr ""
msgid "Templates" msgid "Templates"
msgstr "" msgstr ""
......
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