Commit f7007a7f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'service-desk-templates' into 'master'

Add ability to selected inherited service desk templates from the group

See merge request gitlab-org/gitlab!71176
parents f4e66f2e 9887a361
......@@ -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