Commit 0df9f800 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch '288339-avoid-mutating-incomingemail-prop-in-service_desk_root-vue' into 'master'

Improve service desk settings frontend code

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