Commit 48b29935 authored by Coung Ngo's avatar Coung Ngo Committed by Brandon Labuschagne

Improve service desk frontend code

Improve code as a result of
https://gitlab.com/gitlab-org/gitlab/-/issues/288339

This commit:

- Fixes `service_desk_root.vue` `incomingEmail` prop from being mutated
- Removes data properties in the Vue root component
- Converts root component props to provide/inject
- Removes an event hub in favour of component events
- Removes service class
- Completely overhauls the related tests
parent ca67c9c7
......@@ -2,60 +2,42 @@
import { GlAlert } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ServiceDeskSetting from './service_desk_setting.vue';
import ServiceDeskService from '../services/service_desk_service';
import eventHub from '../event_hub';
import axios from '~/lib/utils/axios_utils';
export default {
name: 'ServiceDeskRoot',
components: {
GlAlert,
ServiceDeskSetting,
},
props: {
inject: {
initialIsEnabled: {
type: Boolean,
required: true,
default: false,
},
endpoint: {
type: String,
required: true,
default: '',
},
incomingEmail: {
type: String,
required: false,
initialIncomingEmail: {
default: '',
},
customEmail: {
type: String,
required: false,
default: '',
},
customEmailEnabled: {
type: Boolean,
required: false,
default: false,
},
selectedTemplate: {
type: String,
required: false,
default: '',
},
outgoingName: {
type: String,
required: false,
default: '',
},
projectKey: {
type: String,
required: false,
default: '',
},
templates: {
type: Array,
required: false,
default: () => [],
default: [],
},
},
data() {
return {
isEnabled: this.initialIsEnabled,
......@@ -63,28 +45,21 @@ export default {
isAlertShowing: false,
alertVariant: 'danger',
alertMessage: '',
incomingEmail: this.initialIncomingEmail,
updatedCustomEmail: this.customEmail,
};
},
created() {
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
eventHub.$on('serviceDeskTemplateSave', this.onSaveTemplate);
this.service = new ServiceDeskService(this.endpoint);
},
beforeDestroy() {
eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
eventHub.$off('serviceDeskTemplateSave', this.onSaveTemplate);
},
methods: {
onEnableToggled(isChecked) {
this.isEnabled = isChecked;
this.incomingEmail = '';
this.service
.toggleServiceDesk(isChecked)
const body = {
service_desk_enabled: isChecked,
};
return axios
.put(this.endpoint, body)
.then(({ data }) => {
const email = data.service_desk_address;
if (isChecked && !email) {
......@@ -104,8 +79,16 @@ export default {
onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) {
this.isTemplateSaving = true;
this.service
.updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled)
const body = {
issue_template_key: selectedTemplate,
outgoing_name: outgoingName,
project_key: projectKey,
service_desk_enabled: this.isEnabled,
};
return axios
.put(this.endpoint, body)
.then(({ data }) => {
this.updatedCustomEmail = data?.service_desk_address;
this.showAlert(__('Changes saved.'), 'success');
......@@ -150,6 +133,8 @@ export default {
:initial-project-key="projectKey"
:templates="templates"
:is-template-saving="isTemplateSaving"
@save="onSaveTemplate"
@toggle="onEnableToggled"
/>
</div>
</template>
<script>
import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskSetting',
components: {
ClipboardButton,
GlButton,
......@@ -15,7 +12,6 @@ export default {
GlLoadingIcon,
GlSprintf,
},
mixins: [glFeatureFlagsMixin()],
props: {
isEnabled: {
type: Boolean,
......@@ -84,10 +80,10 @@ export default {
},
methods: {
onCheckboxToggle(isChecked) {
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
this.$emit('toggle', isChecked);
},
onSaveTemplate() {
eventHub.$emit('serviceDeskTemplateSave', {
this.$emit('save', {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
......@@ -111,7 +107,11 @@ export default {
</label>
<div v-if="isEnabled" class="row mt-3">
<div class="col-md-9 mb-0">
<strong id="incoming-email-describer" class="d-block mb-1">
<strong
id="incoming-email-describer"
class="gl-display-block gl-mb-1"
data-testid="incoming-email-describer"
>
{{ __('Email address to use for Support Desk') }}
</strong>
<template v-if="email">
......@@ -128,11 +128,7 @@ export default {
disabled="true"
/>
<div class="input-group-append">
<clipboard-button
:title="__('Copy')"
:text="email"
css-class="input-group-text qa-clipboard-button"
/>
<clipboard-button :title="__('Copy')" :text="email" css-class="input-group-text" />
</div>
</div>
<span v-if="hasCustomEmail" class="form-text text-muted">
......
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
......@@ -3,43 +3,37 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import ServiceDeskRoot from './components/service_desk_root.vue';
export default () => {
const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root');
if (serviceDeskRootElement) {
// eslint-disable-next-line no-new
new Vue({
el: serviceDeskRootElement,
components: {
ServiceDeskRoot,
},
data() {
const { dataset } = serviceDeskRootElement;
return {
initialIsEnabled: parseBoolean(dataset.enabled),
endpoint: dataset.endpoint,
incomingEmail: dataset.incomingEmail,
customEmail: dataset.customEmail,
customEmailEnabled: parseBoolean(dataset.customEmailEnabled),
selectedTemplate: dataset.selectedTemplate,
outgoingName: dataset.outgoingName,
projectKey: dataset.projectKey,
templates: JSON.parse(dataset.templates),
};
},
render(createElement) {
return createElement('service-desk-root', {
props: {
initialIsEnabled: this.initialIsEnabled,
endpoint: this.endpoint,
incomingEmail: this.incomingEmail,
customEmail: this.customEmail,
customEmailEnabled: this.customEmailEnabled,
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
templates: this.templates,
},
});
const el = document.querySelector('.js-service-desk-setting-root');
if (!el) {
return false;
}
const {
customEmail,
customEmailEnabled,
enabled,
endpoint,
incomingEmail,
outgoingName,
projectKey,
selectedTemplate,
templates,
} = el.dataset;
return new Vue({
el,
provide: {
customEmail,
customEmailEnabled: parseBoolean(customEmailEnabled),
endpoint,
initialIncomingEmail: incomingEmail,
initialIsEnabled: parseBoolean(enabled),
outgoingName,
projectKey,
selectedTemplate,
templates: JSON.parse(templates),
},
render: (createElement) => createElement(ServiceDeskRoot),
});
}
};
import axios from '~/lib/utils/axios_utils';
class ServiceDeskService {
constructor(endpoint) {
this.endpoint = endpoint;
}
toggleServiceDesk(enable) {
return axios.put(this.endpoint, { service_desk_enabled: enable });
}
updateTemplate({ selectedTemplate, outgoingName, projectKey = '' }, isEnabled) {
const body = {
issue_template_key: selectedTemplate,
outgoing_name: outgoingName,
project_key: projectKey,
service_desk_enabled: isEnabled,
};
return axios.put(this.endpoint, body);
}
}
export default ServiceDeskService;
import { mount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
describe('ServiceDeskRoot', () => {
const endpoint = '/gitlab-org/gitlab-test/service_desk';
const initialIncomingEmail = 'servicedeskaddress@example.com';
let axiosMock;
let wrapper;
let spy;
const provideData = {
customEmail: 'custom.email@example.com',
customEmailEnabled: true,
endpoint: '/gitlab-org/gitlab-test/service_desk',
initialIncomingEmail: 'servicedeskaddress@example.com',
initialIsEnabled: true,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
selectedTemplate: 'Bug',
templates: ['Bug', 'Documentation'],
};
const getAlertText = () => wrapper.find(GlAlert).text();
const createComponent = () => shallowMount(ServiceDeskRoot, { provide: provideData });
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
spy = jest.spyOn(axios, 'put');
});
afterEach(() => {
......@@ -25,156 +41,122 @@ describe('ServiceDeskRoot', () => {
}
});
it('sends a request to toggle service desk off when the toggle is clicked from the on state', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
describe('ServiceDeskSetting component', () => {
it('is rendered', () => {
wrapper = createComponent();
spy = jest.spyOn(axios, 'put');
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
initialIncomingEmail,
endpoint,
},
expect(wrapper.find(ServiceDeskSetting).props()).toEqual({
customEmail: provideData.customEmail,
customEmailEnabled: provideData.customEmailEnabled,
incomingEmail: provideData.initialIncomingEmail,
initialOutgoingName: provideData.outgoingName,
initialProjectKey: provideData.projectKey,
initialSelectedTemplate: provideData.selectedTemplate,
isEnabled: provideData.initialIsEnabled,
isTemplateSaving: false,
templates: provideData.templates,
});
});
wrapper.find('button.gl-toggle').trigger('click');
describe('toggle event', () => {
describe('when toggling service desk on', () => {
beforeEach(async () => {
wrapper = createComponent();
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(spy).toHaveBeenCalledWith(endpoint, { service_desk_enabled: false });
});
wrapper.find(ServiceDeskSetting).vm.$emit('toggle', true);
await waitForPromises();
});
it('sends a request to toggle service desk on when the toggle is clicked from the off state', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
it('sends a request to turn service desk on', () => {
axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
spy = jest.spyOn(axios, 'put');
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: false,
initialIncomingEmail: '',
endpoint,
},
expect(spy).toHaveBeenCalledWith(provideData.endpoint, { service_desk_enabled: true });
});
wrapper.find('button.gl-toggle').trigger('click');
it('shows a message when there is an error', () => {
axiosMock.onPut(provideData.endpoint).networkError();
return wrapper.vm.$nextTick(() => {
expect(spy).toHaveBeenCalledWith(endpoint, { service_desk_enabled: true });
expect(getAlertText()).toContain('An error occurred while enabling Service Desk.');
});
});
describe('when toggling service desk off', () => {
beforeEach(async () => {
wrapper = createComponent();
wrapper.find(ServiceDeskSetting).vm.$emit('toggle', false);
await waitForPromises();
});
it('shows an error message when there is an issue toggling service desk on', () => {
axiosMock.onPut(endpoint).networkError();
it('sends a request to turn service desk off', () => {
axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: false,
initialIncomingEmail: '',
endpoint,
},
expect(spy).toHaveBeenCalledWith(provideData.endpoint, { service_desk_enabled: false });
});
wrapper.find('button.gl-toggle').trigger('click');
it('shows a message when there is an error', () => {
axiosMock.onPut(provideData.endpoint).networkError();
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.html()).toContain('An error occurred while enabling Service Desk.');
expect(getAlertText()).toContain('An error occurred while disabling Service Desk.');
});
});
});
it('sends a request to update template when the "Save template" button is clicked', () => {
axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
describe('save event', () => {
describe('successful request', () => {
beforeEach(async () => {
axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
spy = jest.spyOn(axios, 'put');
wrapper = createComponent();
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
endpoint,
initialIncomingEmail,
const payload = {
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
templates: ['Bug', 'Documentation'],
projectKey: 'key',
},
});
};
wrapper.find('button.btn-success').trigger('click');
wrapper.find(ServiceDeskSetting).vm.$emit('save', payload);
return wrapper.vm.$nextTick(() => {
expect(spy).toHaveBeenCalledWith(endpoint, {
await waitForPromises();
});
it('sends a request to update template', async () => {
expect(spy).toHaveBeenCalledWith(provideData.endpoint, {
issue_template_key: 'Bug',
outgoing_name: 'GitLab Support Bot',
project_key: 'key',
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'],
},
});
wrapper.find('button.btn-success').trigger('click');
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.html()).toContain('Changes saved.');
it('shows success message', () => {
expect(getAlertText()).toContain('Changes saved.');
});
});
it('shows an error message when there is an issue saving the template', () => {
axiosMock.onPut(endpoint).networkError();
describe('unsuccessful request', () => {
beforeEach(async () => {
axiosMock.onPut(provideData.endpoint).networkError();
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
endpoint,
initialIncomingEmail,
wrapper = createComponent();
const payload = {
selectedTemplate: 'Bug',
templates: ['Bug', 'Documentation'],
},
});
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
};
wrapper.find('button.btn-success').trigger('click');
wrapper.find(ServiceDeskSetting).vm.$emit('save', payload);
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.html()).toContain('An error occured while saving changes:');
await waitForPromises();
});
});
it('passes customEmail through updatedCustomEmail correctly', () => {
const customEmail = 'foo';
wrapper = mount(ServiceDeskRoot, {
propsData: {
initialIsEnabled: true,
endpoint,
customEmail,
},
it('shows an error message', () => {
expect(getAlertText()).toContain('An error occured while saving changes:');
});
});
});
expect(wrapper.find(ServiceDeskSetting).props('customEmail')).toEqual(customEmail);
});
});
import AxiosMockAdapter from 'axios-mock-adapter';
import ServiceDeskService from '~/projects/settings_service_desk/services/service_desk_service';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
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('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(
{
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
},
true,
)
.then((response) => {
expect(response.data).toEqual(dummyResponse);
});
});
it('fails on error response', () => {
axiosMock.onPut(endpoint).networkError();
return service
.updateTemplate(
{
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
},
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(
{
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
},
true,
);
expect(spy).toHaveBeenCalledWith(endpoint, {
issue_template_key: 'Bug',
outgoing_name: 'GitLab Support Bot',
project_key: 'key',
service_desk_enabled: true,
});
spy.mockRestore();
});
});
});
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