Commit 9887a361 authored by Rajat Jain's avatar Rajat Jain

Render inherited templates from group in service desk

Right now, only project level templates are rendered in the service desk.
With the introduction of inheritance, a project has access to group level
templates as well. With this change, you can see all the templates available
-- project level + group level.

Changelog: added
parent 7d567806
......@@ -31,6 +31,9 @@ export default {
selectedTemplate: {
default: '',
},
selectedFileTemplateProjectId: {
default: null,
},
outgoingName: {
default: '',
},
......@@ -80,7 +83,7 @@ export default {
});
},
onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) {
onSaveTemplate({ selectedTemplate, fileTemplateProjectId, outgoingName, projectKey }) {
this.isTemplateSaving = true;
const body = {
......@@ -88,6 +91,7 @@ export default {
outgoing_name: outgoingName,
project_key: projectKey,
service_desk_enabled: this.isEnabled,
file_template_project_id: fileTemplateProjectId,
};
return axios
......@@ -132,6 +136,7 @@ export default {
:custom-email="updatedCustomEmail"
:custom-email-enabled="customEmailEnabled"
:initial-selected-template="selectedTemplate"
:initial-selected-file-template-project-id="selectedFileTemplateProjectId"
:initial-outgoing-name="outgoingName"
:initial-project-key="projectKey"
:templates="templates"
......
<script>
import {
GlButton,
GlFormSelect,
GlToggle,
GlLoadingIcon,
GlSprintf,
GlFormInput,
GlLink,
} from '@gitlab/ui';
import { GlButton, GlToggle, GlLoadingIcon, GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue';
export default {
i18n: {
......@@ -18,12 +11,12 @@ export default {
components: {
ClipboardButton,
GlButton,
GlFormSelect,
GlToggle,
GlLoadingIcon,
GlSprintf,
GlFormInput,
GlLink,
ServiceDeskTemplateDropdown,
},
props: {
isEnabled: {
......@@ -49,6 +42,11 @@ export default {
required: false,
default: '',
},
initialSelectedFileTemplateProjectId: {
type: Number,
required: false,
default: null,
},
initialOutgoingName: {
type: String,
required: false,
......@@ -73,14 +71,13 @@ export default {
data() {
return {
selectedTemplate: this.initialSelectedTemplate,
selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId,
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
projectKey: this.initialProjectKey,
searchTerm: '',
};
},
computed: {
templateOptions() {
return [''].concat(this.templates);
},
hasProjectKeySupport() {
return Boolean(this.customEmailEnabled);
},
......@@ -100,8 +97,13 @@ export default {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
fileTemplateProjectId: this.selectedFileTemplateProjectId,
});
},
templateChange({ selectedFileTemplateProjectId, selectedTemplate }) {
this.selectedFileTemplateProjectId = selectedFileTemplateProjectId;
this.selectedTemplate = selectedTemplate;
},
},
};
</script>
......@@ -193,12 +195,13 @@ export default {
<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"
data-qa-selector="service_desk_template_dropdown"
:options="templateOptions"
<service-desk-template-dropdown
:selected-template="selectedTemplate"
:selected-file-template-project-id="selectedFileTemplateProjectId"
:templates="templates"
@change="templateChange"
/>
<label for="service-desk-email-from-name" class="mt-3">
{{ __('Email display name') }}
</label>
......@@ -210,6 +213,7 @@ export default {
<gl-button
variant="success"
class="gl-mt-5"
data-testid="save_service_desk_settings_button"
data-qa-selector="save_service_desk_settings_button"
:disabled="isTemplateSaving"
@click="onSaveTemplate"
......
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlSearchBoxByType,
},
props: {
selectedTemplate: {
type: String,
required: false,
default: '',
},
templates: {
type: Array,
required: true,
},
selectedFileTemplateProjectId: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
searchTerm: '',
};
},
computed: {
templateOptions() {
if (this.searchTerm) {
const filteredTemplates = [];
for (let i = 0; i < this.templates.length; i += 2) {
const sectionName = this.templates[i];
const availableTemplates = this.templates[i + 1];
const matchedTemplates = fuzzaldrinPlus.filter(availableTemplates, this.searchTerm, {
key: 'name',
});
if (matchedTemplates.length > 0) {
filteredTemplates.push(sectionName, matchedTemplates);
}
}
return filteredTemplates;
}
return this.templates;
},
},
methods: {
templateClick(template) {
// Clicking on the same template should unselect it
if (
template.name === this.selectedTemplate &&
template.project_id === this.selectedFileTemplateProjectId
) {
this.$emit('change', {
selectedFileTemplateProjectId: null,
selectedTemplate: null,
});
return;
}
this.$emit('change', {
selectedFileTemplateProjectId: template.project_id,
selectedTemplate: template.key,
});
},
},
i18n: {
defaultDropdownText: __('Choose a template'),
},
};
</script>
<template>
<gl-dropdown
id="service-desk-template-select"
:text="selectedTemplate || $options.i18n.defaultDropdownText"
:header-text="$options.i18n.defaultDropdownText"
data-qa-selector="service_desk_template_dropdown"
:block="true"
class="service-desk-template-select"
toggle-class="gl-m-0"
>
<template #header>
<gl-search-box-by-type v-model.trim="searchTerm" />
</template>
<template v-for="item in templateOptions">
<gl-dropdown-section-header v-if="!Array.isArray(item)" :key="item">
{{ item }}
</gl-dropdown-section-header>
<template v-else>
<gl-dropdown-item
v-for="template in item"
:key="template.key"
:is-check-item="true"
:is-checked="
template.project_id === selectedFileTemplateProjectId &&
template.name === selectedTemplate
"
@click="() => templateClick(template)"
>
{{ template.name }}
</gl-dropdown-item>
</template>
</template>
</gl-dropdown>
</template>
......@@ -18,6 +18,7 @@ export default () => {
outgoingName,
projectKey,
selectedTemplate,
selectedFileTemplateProjectId,
templates,
} = el.dataset;
......@@ -32,6 +33,7 @@ export default () => {
outgoingName,
projectKey,
selectedTemplate,
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
},
render: (createElement) => createElement(ServiceDeskRoot),
......
......@@ -32,14 +32,17 @@ module IssuablesDescriptionTemplatesHelper
@template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names(project, issuable_type.pluralize)
end
# Overriden on EE::IssuablesDescriptionTemplatesHelper to include inherited templates names
def issuable_templates_names(issuable, include_inherited_templates = false)
def selected_template(issuable)
all_templates = issuable_templates(ref_project, issuable.to_ability_name)
all_templates.values.flatten.map { |tpl| tpl[:name] if tpl[:project_id] == ref_project.id }.compact.uniq
# Only local templates will be listed if licenses for inherited templates are not present
all_templates = all_templates.values.flatten.map { |tpl| tpl[:name] }.compact.uniq
all_templates.find { |tmpl_name| tmpl_name == params[:issuable_template] }
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates_names(issuable, true).any? { |tmpl_name| tmpl_name == params[:issuable_template] }
def available_service_desk_templates_for(project)
issuable_templates(project, 'issue').flatten.to_json
end
def template_names_path(parent, issuable)
......
......@@ -14,8 +14,9 @@
custom_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
custom_email_enabled: "#{Gitlab::ServiceDeskEmail.enabled?}",
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",
templates: issuable_templates_names(Issue.new) } }
templates: available_service_desk_templates_for(@project) } }
- elsif show_callout?('promote_service_desk_dismissed')
= render 'shared/promotions/promote_servicedesk'
......@@ -137,15 +137,23 @@ You can use these placeholders to be automatically replaced in each email:
#### New Service Desk issues
You can select one [issue description template](description_templates.md#create-an-issue-template)
You can select one [description template](description_templates.md#create-an-issue-template)
**per project** to be appended to every new Service Desk issue's description.
Issue description templates should reside in your repository's `.gitlab/issue_templates/` directory.
To use a custom issue template with Service Desk, in your project:
You can set description templates at various levels:
1. [Create a description template](description_templates.md#create-an-issue-template)
1. Go to **Settings > General > Service Desk**.
1. From the dropdown **Template to append to all Service Desk issues**, select your template.
- The entire [instance](description_templates.md#set-instance-level-description-templates).
- A specific [group or subgroup](description_templates.md#set-group-level-description-templates).
- A specific [project](description_templates.md#set-a-default-template-for-merge-requests-and-issues).
The templates are inherited. For example, in a project, you can also access templates set for the instance or the project’s parent groups.
To use a custom description template with Service Desk:
1. On the top bar, select **Menu > Projects** and find your project.
1. [Create a description template](description_templates.md#create-an-issue-template).
1. On the left sidebar, select **Settings > General > Service Desk**.
1. From the dropdown **Template to append to all Service Desk issues**, search or select your template.
### Using a custom email display name
......@@ -156,7 +164,8 @@ this name in the `From` header. The default display name is `GitLab Support Bot`
To edit the custom email display name:
1. In a project, go to **Settings > General > Service Desk**.
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > General > Service Desk**.
1. Enter a new name in **Email display name**.
1. Select **Save Changes**.
......
# frozen_string_literal: true
module EE
module IssuablesDescriptionTemplatesHelper
extend ::Gitlab::Utils::Override
override :issuable_templates_names
def issuable_templates_names(issuable, include_inherited_templates = false)
return super unless include_inherited_templates
all_templates = issuable_templates(ref_project, issuable.to_ability_name)
all_templates.values.flatten.map { |tpl| tpl[:name] }.compact.uniq
end
end
end
......@@ -17,14 +17,22 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
}
end
let_it_be(:issue_template_file) do
{
'.gitlab/issue_templates/template.md' => 'Template file contents'
}
end
let_it_be(:project_with_issue_template) { create(:project, :custom_repo, files: issue_template_file) }
let_it_be_with_reload(:group) { create(:group)}
let_it_be_with_reload(:project) { create(:project, :custom_repo, group: group, files: issuable_project_template_files) }
let_it_be(:group_template_repo) { create(:project, :custom_repo, group: group, files: issuable_group_template_files) }
let_it_be(:group_template_repo) { create(:project, :custom_repo, files: issuable_group_template_files) }
let_it_be(:user) { create(:user) }
let_it_be(:presenter) { project.present(current_user: user) }
before do
stub_licensed_features(custom_file_templates_for_namespace: true, custom_file_templates: true)
stub_ee_application_setting(file_template_project: project_with_issue_template)
project.add_maintainer(user)
sign_in(user)
......@@ -39,9 +47,28 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
expect(proj_instance).to receive(:present).with(current_user: user).and_return(presenter)
end
create(:project_group_link, project: group_template_repo, group: group)
group.update_columns(file_template_project_id: group_template_repo.id)
visit edit_project_path(project)
end
it_behaves_like 'issue description templates from current project only'
it 'loads group, project and instance issue description templates', :aggregate_failures do
within('#service-desk-template-select') do
expect(page).to have_content(:all, 'project-issue-bar')
expect(page).to have_content(:all, 'project-issue-foo')
expect(page).to have_content(:all, 'group-issue-bar')
expect(page).to have_content(:all, 'group-issue-foo')
expect(page).to have_content(:all, 'template')
end
end
it 'persists file_template_project_id on save' do
find('#service-desk-template-select').click
find('.gl-new-dropdown-item-text-primary', exact_text: 'template').click
find('[data-testid="save_service_desk_settings_button"]').click
wait_for_requests
expect(project.service_desk_setting.file_template_project_id).to eq(project_with_issue_template.id)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssuablesDescriptionTemplatesHelper do
include_context 'project issuable templates context'
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:parent_group) { create(:group) }
let_it_be_with_reload(:group) { create(:group, parent: parent_group) }
let_it_be_with_reload(:project) { create(:project, :custom_repo, group: group, files: issuable_template_files) }
let_it_be(:file_template_project) { create(:project, :custom_repo, group: parent_group, files: issuable_template_files) }
let_it_be(:group_member) { create(:group_member, :developer, group: parent_group, user: user) }
let_it_be(:inherited_from) { file_template_project }
shared_examples 'issuable templates' do
context 'when include_inherited_templates is true' do
it 'returns project templates and inherited templates' do
expect(helper.issuable_templates_names(Issue.new, true)).to eq(%w[project_template inherited_template])
end
end
context 'when include_inherited_templates is false' do
it 'returns only project templates' do
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[project_template])
end
end
end
describe '#issuable_templates' do
context 'when project parent group has a file template project' do
before do
stub_licensed_features(custom_file_templates_for_namespace: true)
parent_group.update_columns(file_template_project_id: file_template_project.id)
end
it_behaves_like 'project issuable templates'
end
end
describe '#issuable_template_names' do
let(:templates) do
{
'' => [{ name: 'project_template', id: 'project_issue_template', project_id: project.id }],
'Instance' => [{ name: 'inherited_template', id: 'instance_issue_template', project_id: file_template_project.id }]
}
end
before do
allow(helper).to receive(:ref_project).and_return(project)
allow(helper).to receive(:issuable_templates).and_return(templates)
end
it_behaves_like 'issuable templates'
end
end
export const TEMPLATES = [
'Project #1',
[
{ name: 'Bug', project_id: 1 },
{ name: 'Documentation', project_id: 1 },
{ name: 'Security release', project_id: 1 },
],
];
......@@ -21,6 +21,7 @@ describe('ServiceDeskRoot', () => {
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
selectedTemplate: 'Bug',
selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
};
......@@ -52,6 +53,7 @@ describe('ServiceDeskRoot', () => {
initialOutgoingName: provideData.outgoingName,
initialProjectKey: provideData.projectKey,
initialSelectedTemplate: provideData.selectedTemplate,
initialSelectedFileTemplateProjectId: provideData.selectedFileTemplateProjectId,
isEnabled: provideData.initialIsEnabled,
isTemplateSaving: false,
templates: provideData.templates,
......
import { GlButton, GlFormSelect, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { GlButton, GlDropdown, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
......@@ -13,12 +13,12 @@ describe('ServiceDeskSetting', () => {
const findIncomingEmail = () => wrapper.findByTestId('incoming-email');
const findIncomingEmailLabel = () => wrapper.findByTestId('incoming-email-describer');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findTemplateDropdown = () => wrapper.find(GlFormSelect);
const findTemplateDropdown = () => wrapper.find(GlDropdown);
const findToggle = () => wrapper.find(GlToggle);
const createComponent = ({ props = {}, mountFunction = shallowMount } = {}) =>
const createComponent = ({ props = {} } = {}) =>
extendedWrapper(
mountFunction(ServiceDeskSetting, {
shallowMount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
...props,
......@@ -144,63 +144,6 @@ describe('ServiceDeskSetting', () => {
});
});
});
describe('templates dropdown', () => {
it('renders a dropdown to choose a template', () => {
wrapper = createComponent();
expect(findTemplateDropdown().exists()).toBe(true);
});
it('renders a dropdown with a default value of ""', () => {
wrapper = createComponent({ mountFunction: mount });
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 = createComponent({
props: { initialSelectedTemplate: 'Bug', templates },
mountFunction: mount,
});
expect(findTemplateDropdown().element.value).toEqual('Bug');
});
it('renders a dropdown with no options when the project has no templates', () => {
wrapper = createComponent({
props: { templates: [] },
mountFunction: mount,
});
// The dropdown by default has one empty option
expect(findTemplateDropdown().element.children).toHaveLength(1);
});
it('renders a dropdown with options when the project has templates', () => {
const templates = ['Bug', 'Documentation', 'Security release'];
wrapper = createComponent({
props: { templates },
mountFunction: mount,
});
// 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).toHaveLength(expectedTemplates.length);
expect(dropdownList.includes('Bug')).toEqual(true);
expect(dropdownList.includes('Documentation')).toEqual(true);
expect(dropdownList.includes('Security release')).toEqual(true);
});
});
});
describe('save button', () => {
......@@ -214,6 +157,7 @@ describe('ServiceDeskSetting', () => {
wrapper = createComponent({
props: {
initialSelectedTemplate: 'Bug',
initialSelectedFileTemplateProjectId: 42,
initialOutgoingName: 'GitLab Support Bot',
initialProjectKey: 'key',
},
......@@ -225,6 +169,7 @@ describe('ServiceDeskSetting', () => {
const payload = {
selectedTemplate: 'Bug',
fileTemplateProjectId: 42,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
};
......
import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ServiceDeskTemplateDropdown from '~/projects/settings_service_desk/components/service_desk_setting.vue';
import { TEMPLATES } from './mock_data';
describe('ServiceDeskTemplateDropdown', () => {
let wrapper;
const findTemplateDropdown = () => wrapper.find(GlDropdown);
const createComponent = ({ props = {} } = {}) =>
extendedWrapper(
mount(ServiceDeskTemplateDropdown, {
propsData: {
isEnabled: true,
...props,
},
}),
);
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('templates dropdown', () => {
it('renders a dropdown to choose a template', () => {
wrapper = createComponent();
expect(findTemplateDropdown().exists()).toBe(true);
});
it('renders a dropdown with a default value of "Choose a template"', () => {
wrapper = createComponent();
expect(findTemplateDropdown().props('text')).toEqual('Choose a template');
});
it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
const templates = TEMPLATES;
wrapper = createComponent({
props: { initialSelectedTemplate: 'Bug', initialSelectedTemplateProjectId: 1, templates },
});
expect(findTemplateDropdown().props('text')).toEqual('Bug');
});
it('renders a dropdown with header items', () => {
wrapper = createComponent({
props: { templates: TEMPLATES },
});
const headerItems = wrapper.findAll(GlDropdownSectionHeader);
expect(headerItems).toHaveLength(1);
expect(headerItems.at(0).text()).toBe(TEMPLATES[0]);
});
it('renders a dropdown with options when the project has templates', () => {
const templates = TEMPLATES;
wrapper = createComponent({
props: { templates },
});
const expectedTemplates = templates[1];
const items = wrapper.findAll(GlDropdownItem);
const dropdownList = expectedTemplates.map((_, index) => items.at(index).text());
expect(items).toHaveLength(expectedTemplates.length);
expect(dropdownList.includes('Bug')).toEqual(true);
expect(dropdownList.includes('Documentation')).toEqual(true);
expect(dropdownList.includes('Security release')).toEqual(true);
});
});
});
......@@ -44,7 +44,7 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
end
describe '#issuable_templates_names' do
describe '#selected_template' do
let_it_be(:project) { build(:project) }
before do
......@@ -63,7 +63,14 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
it 'returns project templates' do
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
value = [
"",
[
{ name: "another_issue_template", id: "another_issue_template", project_id: project.id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
]
].to_json
expect(helper.available_service_desk_templates_for(@project)).to eq(value)
end
end
......@@ -71,7 +78,8 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
let(:templates) { {} }
it 'returns empty array' do
expect(helper.issuable_templates_names(Issue.new)).to eq([])
value = [].to_json
expect(helper.available_service_desk_templates_for(@project)).to eq(value)
end
end
end
......
......@@ -3,10 +3,10 @@
RSpec.shared_examples 'issue description templates from current project only' do
it 'loads issue description templates from the project only' do
within('#service-desk-template-select') do
expect(page).to have_content('project-issue-bar')
expect(page).to have_content('project-issue-foo')
expect(page).not_to have_content('group-issue-bar')
expect(page).not_to have_content('group-issue-foo')
expect(page).to have_content(:all, 'project-issue-bar')
expect(page).to have_content(:all, 'project-issue-foo')
expect(page).not_to have_content(:all, 'group-issue-bar')
expect(page).not_to have_content(:all, 'group-issue-foo')
end
end
end
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