Commit 606f6caa authored by Alexandru Croitor's avatar Alexandru Croitor

Rolldown issue and merge request templates to projects in the group

Description templates are a variation of file templates, however
description templates were only visible at the project level in which
those were defined.

This implements the ability to inherit description templates from
projects that define description templates and are set as template
repository at group level.

This will allow to display both inherited description templates
from parent group(s) and instance down to project's own own
description templates

Edit issue is using a different FE(vue app) to be redered vs new issue
form. Update templates controller to return the right templates and in
corresponding format, i.e. hash instead of array.

Fix the edge case when templtes in different groups have the same name,
so that it would be correctly resolved to the correct template.
parent 00d64e00
......@@ -51,7 +51,7 @@ export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selec
export const TAKING_INCIDENT_ACTION_DOCS_LINK =
'/help/operations/metrics/alerts#trigger-actions-from-alerts';
export const ISSUE_TEMPLATES_DOCS_LINK =
'/help/user/project/description_templates#creating-issue-templates';
'/help/user/project/description_templates#create-an-issue-template';
/* PagerDuty integration settings constants */
......
......@@ -307,7 +307,7 @@ export default {
});
},
updateAndShowForm(templates = []) {
updateAndShowForm(templates = {}) {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({
......
......@@ -13,9 +13,9 @@ export default {
required: true,
},
issuableTemplates: {
type: Array,
type: [Object, Array],
required: false,
default: () => [],
default: () => {},
},
projectPath: {
type: String,
......
......@@ -26,9 +26,9 @@ export default {
required: true,
},
issuableTemplates: {
type: Array,
type: [Object, Array],
required: false,
default: () => [],
default: () => {},
},
issuableType: {
type: String,
......@@ -72,7 +72,7 @@ export default {
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
return Object.values(Object(this.issuableTemplates)).length;
},
showLockedWarning() {
return this.formState.lockedWarningVisible && !this.formState.updateLoading;
......
......@@ -11,7 +11,7 @@ export default class Store {
lockedWarningVisible: false,
updateLoading: false,
lock_version: 0,
issuableTemplates: [],
issuableTemplates: {},
};
}
......
......@@ -25,7 +25,7 @@ class Projects::TemplatesController < Projects::ApplicationController
def names
respond_to do |format|
format.json { render json: TemplateFinder.all_template_names_array(project, params[:template_type].to_s.pluralize) }
format.json { render json: TemplateFinder.all_template_names_hash_or_array(project, params[:template_type].to_s) }
end
end
......
......@@ -22,16 +22,26 @@ class TemplateFinder
end
end
# This is temporary and will be removed once we introduce group level inherited templates and
# remove the inherited_issuable_templates FF
def all_template_names_hash_or_array(project, issuable_type)
if project.inherited_issuable_templates_enabled?
all_template_names(project, issuable_type.pluralize)
else
all_template_names_array(project, issuable_type.pluralize)
end
end
def all_template_names(project, type)
return {} if !VENDORED_TEMPLATES.key?(type.to_s) && type.to_s != 'licenses'
build(type, project).template_names
end
# This is issues and merge requests description templates only.
# This will be removed once we introduce group level inherited templates
# This is for issues and merge requests description templates only.
# This will be removed once we introduce group level inherited templates and remove the inherited_issuable_templates FF
def all_template_names_array(project, type)
all_template_names(project, type).values.flatten.uniq
all_template_names(project, type).values.flatten.select { |tmpl| tmpl[:project_id] == project.id }.compact.uniq
end
end
......
......@@ -5,7 +5,8 @@ module IssuablesDescriptionTemplatesHelper
include GitlabRoutingHelper
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
selected_template = selected_template(issuable)
title = selected_template || "Choose a template"
options = {
toggle_class: 'js-issuable-selector',
title: title,
......@@ -15,7 +16,7 @@ module IssuablesDescriptionTemplatesHelper
data: {
data: issuable_templates(ref_project, issuable.to_ability_name),
field_name: 'issuable_template',
selected: selected_template(issuable),
selected: selected_template,
project_id: ref_project.id
}
}
......@@ -28,15 +29,21 @@ module IssuablesDescriptionTemplatesHelper
def issuable_templates(project, issuable_type)
@template_types ||= {}
@template_types[project.id] ||= {}
@template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names_array(project, issuable_type.pluralize)
@template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names_hash_or_array(project, issuable_type)
end
def issuable_templates_names(issuable)
issuable_templates(ref_project, issuable.to_ability_name).map { |template| template[:name] }
all_templates = issuable_templates(ref_project, issuable.to_ability_name)
if ref_project.inherited_issuable_templates_enabled?
all_templates.values.flatten.map { |tpl| tpl[:name] if tpl[:project_id] == ref_project.id }.compact.uniq
else
all_templates.map { |template| template[:name] }
end
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(ref_project, issuable.to_ability_name).any? { |template| template[:name] == params[:issuable_template] }
params[:issuable_template] if issuable_templates_names(issuable).any? { |tmpl_name| tmpl_name == params[:issuable_template] }
end
def template_names_path(parent, issuable)
......
......@@ -2532,6 +2532,10 @@ class Project < ApplicationRecord
Projects::GitGarbageCollectWorker
end
def inherited_issuable_templates_enabled?
Feature.enabled?(:inherited_issuable_templates, self, default_enabled: :yaml)
end
private
def find_service(services, name)
......
---
name: inherited_issuable_templates
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52360
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321247
milestone: '13.9'
type: development
group: group::project management
default_enabled: false
......@@ -100,7 +100,7 @@ GET /projects/:id/templates/:type/:name
| Attribute | Type | Required | Description |
| ---------- | ------ | -------- | ----------- |
| `id` | integer / string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `type` | string | yes| The type `(dockerfiles|gitignores|gitlab_ci_ymls|licenses|issues|merge_requests)` of the template |
| `type` | string | yes| The type of the template. One of: `dockerfiles`, `gitignores`, `gitlab_ci_ymls`, `licenses`, `issues`, or `merge_requests`. |
| `name` | string | yes | The key of the template, as obtained from the collection endpoint |
| `source_template_project_id` | integer | no | The project ID where a given template is being stored. This is useful when multiple templates from different projects have the same name. If multiple templates have the same name, the match from `closest ancestor` is returned if `source_template_project_id` is not specified |
| `project` | string | no | The project name to use when expanding placeholders in the template. Only affects licenses |
......
......@@ -10,8 +10,8 @@ We have implemented standard features that depend on configuration files in the
When implementing new features, please refer to these existing features to avoid conflicts:
- [Custom Dashboards](../operations/metrics/dashboards/index.md#add-a-new-dashboard-to-your-project): `.gitlab/dashboards/`.
- [Issue Templates](../user/project/description_templates.md#creating-issue-templates): `.gitlab/issue_templates/`.
- [Merge Request Templates](../user/project/description_templates.md#creating-merge-request-templates): `.gitlab/merge_request_templates/`.
- [Issue Templates](../user/project/description_templates.md#create-an-issue-template): `.gitlab/issue_templates/`.
- [Merge Request Templates](../user/project/description_templates.md#create-a-merge-request-template): `.gitlab/merge_request_templates/`.
- [GitLab Kubernetes Agents](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/configuration_repository.md#layout): `.gitlab/agents/`.
- [CODEOWNERS](../user/project/code_owners.md#how-to-set-up-code-owners): `.gitlab/CODEOWNERS`.
- [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.
......
......@@ -54,7 +54,7 @@ With Maintainer or higher [permissions](../../user/permissions.md), you can enab
1. Navigate to **Settings > Operations > Incidents** and expand **Incidents**.
1. Check the **Create an incident** checkbox.
1. To customize the incident, select an
[issue template](../../user/project/description_templates.md#creating-issue-templates).
[issue template](../../user/project/description_templates.md#create-an-issue-template).
1. To send [an email notification](paging.md#email-notifications) to users
with [Developer permissions](../../user/permissions.md), select
**Send a separate email notification to Developers**. Email notifications are
......
......@@ -20,7 +20,7 @@ the tiers are no longer mentioned in GitLab documentation:
[per-group charts](../user/project/milestones/index.md#group-burndown-charts)
- [Code owners](../user/project/code_owners.md)
- Description templates:
- [Setting a default template for merge requests and issues](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues)
- [Setting a default template for merge requests and issues](../user/project/description_templates.md#set-a-default-template-for-merge-requests-and-issues)
- [Email from GitLab](../tools/email.md)
- Groups:
- [Creating group memberships via CN](../user/group/index.md#creating-group-links-via-cn)
......
......@@ -37,10 +37,10 @@ To learn how to create templates for various file types in groups, visit
images guidelines, link to the related issue, reviewer name, and so on.
- You can also create issues and merge request templates for different
stages of your workflow, for example, feature proposal, feature improvement, or a bug report.
- You can use an [issue description template](#creating-issue-templates) as a
- You can use an [issue description template](#create-an-issue-template) as a
[Service Desk email template](service_desk.md#new-service-desk-issues).
## Creating issue templates
## Create an issue template
Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
directory in your repository. Commit and push to your default branch.
......@@ -65,13 +65,13 @@ To create the `.gitlab/issue_templates` directory:
To check if this has worked correctly, [create a new issue](issues/managing_issues.md#create-a-new-issue)
and see if you can choose a description template.
## Creating merge request templates
## Create a merge request template
Similarly to issue templates, create a new Markdown (`.md`) file inside the
`.gitlab/merge_request_templates/` directory in your repository. Commit and
push to your default branch.
## Using the templates
## Use the templates
Let's take for example that you've created the file `.gitlab/issue_templates/Bug.md`.
This enables the `Bug` dropdown option when creating or editing issues. When
......@@ -85,9 +85,45 @@ For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_templat
![Description templates](img/description_templates.png)
## Setting a default template for merge requests and issues **(PREMIUM)**
### Set an issue and merge request description template at group level **(PREMIUM)**
> - Moved to GitLab Premium in 13.9.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52360) in GitLab 13.9.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's disabled by default on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to
[enable it](#enable-or-disable-issue-and-merge-request-description-templates-at-group-and-instance-level).
Templates can be useful because you can create a template once and use it multiple times.
To re-use templates [you've created](../project/description_templates.md#create-an-issue-template):
1. Go to the group's **Settings > General > Templates**.
1. From the dropdown, select your template project as the template repository at group level.
![Group template settings](../group/img/group_file_template_settings.png)
### Set an issue and merge request description template at instance level **(PREMIUM ONLY)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52360) in GitLab 13.9.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's disabled by default on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to
[enable it](#enable-or-disable-issue-and-merge-request-description-templates-at-group-and-instance-level).
Similar to group templates, issue and merge request templates can also be set up at the instance level.
This results in those templates being available in all projects within the instance.
Only instance administrators can set instance-level templates.
To set the instance-level description template repository:
1. Select the **Admin Area** icon (**{admin}**).
1. Go to **Settings > Templates**.
1. From the dropdown, select your template project as the template repository at instance level.
Learn more about [instance template repository](../admin_area/settings/instance_template_repository.md).
![Setting templates in the Admin Area](../admin_area/settings/img/file_template_admin_area.png)
### Set a default template for merge requests and issues **(PREMIUM)**
The visibility of issues or merge requests should be set to either "Everyone
with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the
......@@ -159,3 +195,28 @@ it's very hard to read otherwise.)
/cc @project-manager
/assign @qa-tester
```
## Enable or disable issue and merge request description templates at group and instance level
Setting issue and merge request description templates at group and instance levels
is under development and not ready for production use. It is deployed behind a
feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:inherited_issuable_templates)
```
To disable it:
```ruby
Feature.disable(:inherited_issuable_templates)
```
The feature flag affects these features:
- Setting a templates project as issue and merge request description templates source at group level.
- Setting a templates project as issue and merge request description templates source at instance level.
......@@ -184,7 +184,7 @@ You can then see issue statuses in the [issue list](#issues-list) and the
## Other Issue actions
- [Create an issue from a template](../../project/description_templates.md#using-the-templates)
- [Create an issue from a template](../../project/description_templates.md#use-the-templates)
- [Set a due date](due_dates.md)
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues
in order to change their status, assignee, milestone, or labels in bulk.
......
......@@ -137,13 +137,13 @@ 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#creating-issue-templates)
You can select one [issue 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:
1. [Create a description template](description_templates.md#creating-issue-templates)
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.
......
......@@ -102,7 +102,7 @@ To edit a file:
in the bottom-right corner.
1. When you're done, click **Submit changes...**.
1. (Optional) Adjust the default title and description of the merge request, to submit
with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#creating-merge-request-templates)
with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#create-a-merge-request-template)
from the dropdown menu and edit it accordingly.
1. Click **Submit changes**.
1. A new merge request is automatically created and you can assign a colleague for review.
......
......@@ -17,8 +17,8 @@ module EE
def initialize(type, project, *args, &blk)
super
if CUSTOM_TEMPLATES.key?(type)
finder = CUSTOM_TEMPLATES.fetch(type)
if custom_templates_mapping.key?(type)
finder = custom_templates_mapping.fetch(type)
@custom_templates = ::Gitlab::CustomFileTemplates.new(finder, project)
end
end
......@@ -42,5 +42,18 @@ module EE
# from ancestor group levels
custom_templates.all_template_names.merge(super)
end
# This method is going to be removed once we remove the `inherited_issuable_templates` FF and
# issues and merge_requests entries will go into CUSTOM_TEMPLATES
def custom_templates_mapping
if project&.inherited_issuable_templates_enabled?
CUSTOM_TEMPLATES.merge(
issues: ::Gitlab::Template::IssueTemplate,
merge_requests: ::Gitlab::Template::MergeRequestTemplate
)
else
CUSTOM_TEMPLATES
end
end
end
end
......@@ -4,7 +4,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Default description template for issues')
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/description_templates', anchor: 'setting-a-default-template-for-merge-requests-and-issues') }
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/description_templates', anchor: 'set-a-default-template-for-merge-requests-and-issues') }
%p#issue-settings-default-template-label= _('Set a default description template to be used for new issues. %{link_start}What are description templates?%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.settings-content
......
......@@ -116,7 +116,7 @@ module Gitlab
def translate(template, project, category:)
return unless template
template.category = category
template.category = category if category
# License templates require special handling as the "vendored" licenses
# are actually in a gem, not on disk like the rest of the templates. So,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
let_it_be(:issuable_project_template_files) do
{
'.gitlab/issue_templates/project-issue-bar.md' => 'Project Issue Template Bar',
'.gitlab/issue_templates/project-issue-foo.md' => 'Project Issue Template Foo'
}
end
let_it_be(:issuable_group_template_files) do
{
'.gitlab/issue_templates/group-issue-bar.md' => 'Group Issue Template Bar',
'.gitlab/issue_templates/group-issue-foo.md' => 'Group Issue Template Foo'
}
end
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(: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)
project.add_maintainer(user)
sign_in(user)
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?) { true }
allow(::Gitlab::ServiceDeskEmail).to receive(:address_for_key) { 'address-suffix@example.com' }
allow_next_instance_of(Project) do |proj_instance|
expect(proj_instance).to receive(:present).with(current_user: user).and_return(presenter)
end
group.update_columns(file_template_project_id: group_template_repo.id)
end
context 'when inherited_issuable_templates enabled' do
before do
stub_feature_flags(inherited_issuable_templates: true)
visit edit_project_path(project)
end
it_behaves_like 'issue description templates from current project only'
end
context 'when inherited_issuable_templates disabled' do
before do
stub_feature_flags(inherited_issuable_templates: false)
visit edit_project_path(project)
end
it_behaves_like 'issue description templates from current project only'
end
end
......@@ -20,6 +20,8 @@ RSpec.describe TemplateFinder do
:dockerfiles | ::Gitlab::Template::CustomDockerfileTemplate
:gitignores | ::Gitlab::Template::CustomGitignoreTemplate
:gitlab_ci_ymls | ::Gitlab::Template::CustomGitlabCiYmlTemplate
:issues | ::Gitlab::Template::IssueTemplate
:merge_requests | ::Gitlab::Template::MergeRequestTemplate
end
with_them do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssuablesDescriptionTemplatesHelper do
include_context 'project issuable templates context'
describe '#issuable_templates' do
context 'when project parent group has a file template project' do
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 }
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
end
......@@ -27,20 +27,34 @@ RSpec.describe "Custom file template classes" do
'Dockerfile/category/baz.txt' => 'CustomDockerfileTemplate category baz',
'gitignore/category/baz.txt' => 'CustomGitignoreTemplate category baz',
'gitlab-ci/category/baz.yml' => 'CustomGitlabCiYmlTemplate category baz',
'LICENSE/category/baz.txt' => 'CustomLicenseTemplate category baz'
'LICENSE/category/baz.txt' => 'CustomLicenseTemplate category baz',
'.gitlab/issue_templates/bar.md' => 'IssueTemplate Bar',
'.gitlab/issue_templates/foo.md' => 'IssueTemplate Foo',
'.gitlab/issue_templates/bad.txt' => 'IssueTemplate Bad',
'.gitlab/issue_templates/baz.xyz' => 'IssueTemplate Baz',
'.gitlab/merge_request_templates/bar.md' => 'MergeRequestTemplate Bar',
'.gitlab/merge_request_templates/foo.md' => 'MergeRequestTemplate Foo',
'.gitlab/merge_request_templates/bad.txt' => 'MergeRequestTemplate Bad',
'.gitlab/merge_request_templates/baz.xyz' => 'MergeRequestTemplate Baz'
}
let_it_be(:project) { create(:project, :custom_repo, files: files) }
[
::Gitlab::Template::CustomDockerfileTemplate,
::Gitlab::Template::CustomGitignoreTemplate,
::Gitlab::Template::CustomGitlabCiYmlTemplate,
::Gitlab::Template::CustomLicenseTemplate,
::Gitlab::Template::CustomMetricsDashboardYmlTemplate
].each do |template_class|
describe template_class do
let(:name) { template_class.name.demodulize }
custom_templates = [
{ class_name: ::Gitlab::Template::CustomDockerfileTemplate, category: 'Custom' },
{ class_name: ::Gitlab::Template::CustomGitignoreTemplate, category: 'Custom' },
{ class_name: ::Gitlab::Template::CustomGitlabCiYmlTemplate, category: 'Custom' },
{ class_name: ::Gitlab::Template::CustomLicenseTemplate, category: 'Custom' },
{ class_name: ::Gitlab::Template::CustomMetricsDashboardYmlTemplate, category: 'Custom' },
{ class_name: ::Gitlab::Template::IssueTemplate, category: 'Project Templates' },
{ class_name: ::Gitlab::Template::MergeRequestTemplate, category: 'Project Templates' }
].freeze
custom_templates.each do |template_class|
describe template_class[:class_name] do
let(:name) { template_class[:class_name].name.demodulize }
describe '.all' do
it 'returns all valid templates' do
......@@ -48,7 +62,7 @@ RSpec.describe "Custom file template classes" do
aggregate_failures do
expect(found.map(&:name)).to contain_exactly('foo', 'bar')
expect(found.map(&:category).uniq).to contain_exactly('Custom')
expect(found.map(&:category).uniq).to contain_exactly(template_class[:category])
end
end
end
......
......@@ -87,11 +87,11 @@ module Gitlab
raise NotImplementedError
end
def by_category(category, project = nil)
def by_category(category, project = nil, empty_category_title: nil)
directory = category_directory(category)
files = finder(project).list_files_for(directory)
files.map { |f| new(f, project, category: category) }.sort
files.map { |f| new(f, project, category: category.presence || empty_category_title) }.sort
end
def category_directory(category)
......
......@@ -25,6 +25,10 @@ module Gitlab
# follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/300279
project.repository.issue_template_names_by_category
end
def by_category(category, project = nil, empty_category_title: nil)
super(category, project, empty_category_title: _('Project Templates'))
end
end
end
end
......
......@@ -25,6 +25,10 @@ module Gitlab
# follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/300279
project.repository.merge_request_template_names_by_category
end
def by_category(category, project = nil, empty_category_title: nil)
super(category, project, empty_category_title: _('Project Templates'))
end
end
end
end
......
......@@ -22961,6 +22961,9 @@ msgstr ""
msgid "Project ID"
msgstr ""
msgid "Project Templates"
msgstr ""
msgid "Project URL"
msgstr ""
......
......@@ -160,13 +160,28 @@ RSpec.describe Projects::TemplatesController do
end
shared_examples 'template names request' do
it 'returns the template names' do
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
context 'when feature flag enabled' do
it 'returns the template names', :aggregate_failures do
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
expect(json_response.size).to eq(2)
expect(json_response.map { |x| x.slice('name') }).to match(expected_template_names)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['Project Templates'].size).to eq(2)
expect(json_response['Project Templates'].map { |x| x.slice('name') }).to match(expected_template_names)
end
end
context 'when feature flag disabled' do
before do
stub_feature_flags(inherited_issuable_templates: false)
end
it 'returns the template names', :aggregate_failures do
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
expect(json_response.map { |x| x.slice('name') }).to match(expected_template_names)
end
end
it 'fails for user with no access' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Service Desk Setting', :js do
RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
let(:project) { create(:project_empty_repo, :private, service_desk_enabled: false) }
let(:presenter) { project.present(current_user: user) }
let(:user) { create(:user) }
......@@ -66,5 +66,48 @@ RSpec.describe 'Service Desk Setting', :js do
expect(find('[data-testid="incoming-email"]').value).to eq('address-suffix@example.com')
end
context 'issue description templates' do
let_it_be(:issuable_project_template_files) do
{
'.gitlab/issue_templates/project-issue-bar.md' => 'Project Issue Template Bar',
'.gitlab/issue_templates/project-issue-foo.md' => 'Project Issue Template Foo'
}
end
let_it_be(:issuable_group_template_files) do
{
'.gitlab/issue_templates/group-issue-bar.md' => 'Group Issue Template Bar',
'.gitlab/issue_templates/group-issue-foo.md' => 'Group Issue Template Foo'
}
end
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) }
before do
stub_licensed_features(custom_file_templates_for_namespace: false, custom_file_templates: false)
group.update_columns(file_template_project_id: group_template_repo.id)
end
context 'when inherited_issuable_templates enabled' do
before do
stub_feature_flags(inherited_issuable_templates: true)
visit edit_project_path(project)
end
it_behaves_like 'issue description templates from current project only'
end
context 'when inherited_issuable_templates disabled' do
before do
stub_feature_flags(inherited_issuable_templates: false)
visit edit_project_path(project)
end
it_behaves_like 'issue description templates from current project only'
end
end
end
end
......@@ -35,7 +35,7 @@ exports[`Alert integration settings form default state should match the default
Incident template (optional)
<gl-link-stub
href="/help/user/project/description_templates#creating-issue-templates"
href="/help/user/project/description_templates#create-an-issue-template"
target="_blank"
>
<gl-icon-stub
......
......@@ -422,7 +422,18 @@ describe('Issuable output', () => {
formSpy = jest.spyOn(wrapper.vm, 'updateAndShowForm');
});
it('shows the form if template names request is successful', () => {
it('shows the form if template names as hash request is successful', () => {
const mockData = {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
};
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
expect(formSpy).toHaveBeenCalledWith(mockData);
});
});
it('shows the form if template names as array request is successful', () => {
const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }];
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
......
import Vue from 'vue';
import descriptionTemplate from '~/issue_show/components/fields/description_template.vue';
describe('Issue description template component', () => {
describe('Issue description template component with templates as hash', () => {
let vm;
let formState;
......@@ -14,7 +14,9 @@ describe('Issue description template component', () => {
vm = new Component({
propsData: {
formState,
issuableTemplates: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
issuableTemplates: {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
},
projectId: 1,
projectPath: '/',
namespacePath: '/',
......@@ -23,9 +25,9 @@ describe('Issue description template component', () => {
}).$mount();
});
it('renders templates as JSON array in data attribute', () => {
it('renders templates as JSON hash in data attribute', () => {
expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
'[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]',
'{"test":[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]}',
);
});
......@@ -41,3 +43,32 @@ describe('Issue description template component', () => {
expect(vm.issuableTemplate.editor.getValue()).toBe('testing new template');
});
});
describe('Issue description template component with templates as array', () => {
let vm;
let formState;
beforeEach(() => {
const Component = Vue.extend(descriptionTemplate);
formState = {
description: 'test',
};
vm = new Component({
propsData: {
formState,
issuableTemplates: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
projectId: 1,
projectPath: '/',
namespacePath: '/',
projectNamespace: '/',
},
}).$mount();
});
it('renders templates as JSON array in data attribute', () => {
expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
'[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]',
);
});
});
......@@ -42,7 +42,7 @@ describe('Inline edit form component', () => {
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).toBeNull();
});
it('renders template selector when templates exists', () => {
it('renders template selector when templates as array exists', () => {
createComponent({
issuableTemplates: [
{ name: 'test', id: 'test', project_path: 'test', namespace_path: 'test' },
......@@ -52,6 +52,16 @@ describe('Inline edit form component', () => {
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
});
it('renders template selector when templates as hash exists', () => {
createComponent({
issuableTemplates: {
test: [{ name: 'test', id: 'test', project_path: 'test', namespace_path: 'test' }],
},
});
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
});
it('hides locked warning by default', () => {
createComponent();
......
......@@ -13,22 +13,33 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
let_it_be(:group_member) { create(:group_member, :developer, group: parent_group, user: user) }
let_it_be(:project_member) { create(:project_member, :developer, user: user, project: project) }
it 'returns empty hash when template type does not exist' do
expect(helper.issuable_templates(build(:project), 'non-existent-template-type')).to eq([])
context 'when feature flag disabled' do
before do
stub_feature_flags(inherited_issuable_templates: false)
end
it 'returns empty array when template type does not exist' do
expect(helper.issuable_templates(project, 'non-existent-template-type')).to eq([])
end
end
context 'with cached issuable templates' do
context 'when feature flag enabled' do
before do
allow(Gitlab::Template::IssueTemplate).to receive(:template_names).and_return({})
allow(Gitlab::Template::MergeRequestTemplate).to receive(:template_names).and_return({})
stub_feature_flags(inherited_issuable_templates: true)
end
helper.issuable_templates(project, 'issues')
helper.issuable_templates(project, 'merge_request')
it 'returns empty hash when template type does not exist' do
expect(helper.issuable_templates(build(:project), 'non-existent-template-type')).to eq({})
end
end
context 'with cached issuable templates' do
it 'does not call TemplateFinder' do
expect(Gitlab::Template::IssueTemplate).not_to receive(:template_names)
expect(Gitlab::Template::MergeRequestTemplate).not_to receive(:template_names)
expect(Gitlab::Template::IssueTemplate).to receive(:template_names).once.and_call_original
expect(Gitlab::Template::MergeRequestTemplate).to receive(:template_names).once.and_call_original
helper.issuable_templates(project, 'issues')
helper.issuable_templates(project, 'merge_request')
helper.issuable_templates(project, 'issues')
helper.issuable_templates(project, 'merge_request')
end
......@@ -63,29 +74,78 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
describe '#issuable_templates_names' do
let(:project) { double(Project, id: 21) }
let(:templates) do
[
{ name: "another_issue_template", id: "another_issue_template", project_id: project.id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
]
end
let_it_be(:project) { build(:project) }
it 'returns project templates only' do
before do
allow(helper).to receive(:ref_project).and_return(project)
allow(helper).to receive(:issuable_templates).and_return(templates)
end
context 'when feature flag disabled' do
let(:templates) do
[
{ name: "another_issue_template", id: "another_issue_template", project_id: project.id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
]
end
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
before do
stub_feature_flags(inherited_issuable_templates: false)
end
it 'returns project templates only' do
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
end
end
context 'when feature flag enabled' do
before do
stub_feature_flags(inherited_issuable_templates: true)
end
context 'with matching project templates' do
let(:templates) do
{
"" => [
{ name: "another_issue_template", id: "another_issue_template", project_id: project.id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
],
"Instance" => [
{ name: "first_issue_issue_template", id: "first_issue_issue_template", project_id: non_existing_record_id },
{ name: "second_instance_issue_template", id: "second_instance_issue_template", project_id: non_existing_record_id }
]
}
end
it 'returns project templates only' do
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
end
end
context 'without matching project templates' do
let(:templates) do
{
"Project Templates" => [
{ name: "another_issue_template", id: "another_issue_template", project_id: non_existing_record_id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: non_existing_record_id }
],
"Instance" => [
{ name: "first_issue_issue_template", id: "first_issue_issue_template", project_id: non_existing_record_id },
{ name: "second_instance_issue_template", id: "second_instance_issue_template", project_id: non_existing_record_id }
]
}
end
it 'returns empty array' do
expect(helper.issuable_templates_names(Issue.new)).to eq([])
end
end
end
context 'when there are not templates in the project' do
let(:templates) { {} }
it 'returns empty array' do
allow(helper).to receive(:ref_project).and_return(project)
allow(helper).to receive(:issuable_templates).and_return(templates)
expect(helper.issuable_templates_names(Issue.new)).to eq([])
end
end
......
......@@ -23,11 +23,11 @@ RSpec.shared_examples 'project issuable templates' do
end
it 'returns only md files as issue templates' do
expect(helper.issuable_templates(project, 'issue')).to eq(templates('issue', project))
expect(helper.issuable_templates(project, 'issue')).to eq(expected_templates('issue'))
end
it 'returns only md files as merge_request templates' do
expect(helper.issuable_templates(project, 'merge_request')).to eq(templates('merge_request', project))
expect(helper.issuable_templates(project, 'merge_request')).to eq(expected_templates('merge_request'))
end
end
......
# frozen_string_literal: true
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')
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