Commit c5f3664a authored by Markus Koller's avatar Markus Koller

Merge branch '7749-rolldown-the-hierarchy-issuable-templates' into 'master'

Rolldown issue and merge request templates in the group hierarchy

See merge request gitlab-org/gitlab!46222
parents b17744c7 fcc43e30
...@@ -66,6 +66,8 @@ export default class FileTemplateSelector { ...@@ -66,6 +66,8 @@ export default class FileTemplateSelector {
reportSelectionName(options) { reportSelectionName(options) {
const opts = options; const opts = options;
opts.query = options.selectedObj.name; opts.query = options.selectedObj.name;
opts.data = options.selectedObj;
opts.data.source_template_project_id = options.selectedObj.project_id;
this.reportSelection(opts); this.reportSelection(opts);
} }
......
...@@ -30,6 +30,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector { ...@@ -30,6 +30,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
const data = { const data = {
project: this.$dropdown.data('project'), project: this.$dropdown.data('project'),
fullname: this.$dropdown.data('fullname'), fullname: this.$dropdown.data('fullname'),
source_template_project_id: query.project_id,
}; };
this.reportSelection({ this.reportSelection({
......
...@@ -437,6 +437,7 @@ export class GitLabDropdown { ...@@ -437,6 +437,7 @@ export class GitLabDropdown {
groupName = el.data('group'); groupName = el.data('group');
if (groupName) { if (groupName) {
selectedIndex = el.data('index'); selectedIndex = el.data('index');
this.selectedIndex = selectedIndex;
selectedObject = this.renderedData[groupName][selectedIndex]; selectedObject = this.renderedData[groupName][selectedIndex];
} else { } else {
selectedIndex = el.closest('li').index(); selectedIndex = el.closest('li').index();
......
...@@ -51,7 +51,7 @@ export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selec ...@@ -51,7 +51,7 @@ export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selec
export const TAKING_INCIDENT_ACTION_DOCS_LINK = export const TAKING_INCIDENT_ACTION_DOCS_LINK =
'/help/operations/metrics/alerts#trigger-actions-from-alerts'; '/help/operations/metrics/alerts#trigger-actions-from-alerts';
export const ISSUE_TEMPLATES_DOCS_LINK = 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 */ /* PagerDuty integration settings constants */
......
...@@ -132,6 +132,10 @@ export default { ...@@ -132,6 +132,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: Number,
required: true,
},
projectNamespace: { projectNamespace: {
type: String, type: String,
required: true, required: true,
...@@ -303,7 +307,7 @@ export default { ...@@ -303,7 +307,7 @@ export default {
}); });
}, },
updateAndShowForm(templates = []) { updateAndShowForm(templates = {}) {
if (!this.showForm) { if (!this.showForm) {
this.showForm = true; this.showForm = true;
this.store.setFormState({ this.store.setFormState({
...@@ -419,6 +423,7 @@ export default { ...@@ -419,6 +423,7 @@ export default {
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:project-path="projectPath" :project-path="projectPath"
:project-id="projectId"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton" :show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile" :can-attach-file="canAttachFile"
......
...@@ -13,14 +13,18 @@ export default { ...@@ -13,14 +13,18 @@ export default {
required: true, required: true,
}, },
issuableTemplates: { issuableTemplates: {
type: Array, type: Object,
required: false, required: false,
default: () => [], default: () => {},
}, },
projectPath: { projectPath: {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: Number,
required: true,
},
projectNamespace: { projectNamespace: {
type: String, type: String,
required: true, required: true,
...@@ -48,11 +52,12 @@ export default { ...@@ -48,11 +52,12 @@ export default {
</script> </script>
<template> <template>
<div class="dropdown js-issuable-selector-wrap" data-issuable-type="issue"> <div class="dropdown js-issuable-selector-wrap" data-issuable-type="issues">
<button <button
ref="toggle" ref="toggle"
:data-namespace-path="projectNamespace" :data-namespace-path="projectNamespace"
:data-project-path="projectPath" :data-project-path="projectPath"
:data-project-id="projectId"
:data-data="issuableTemplatesJson" :data-data="issuableTemplatesJson"
class="dropdown-menu-toggle js-issuable-selector" class="dropdown-menu-toggle js-issuable-selector"
type="button" type="button"
......
...@@ -26,9 +26,9 @@ export default { ...@@ -26,9 +26,9 @@ export default {
required: true, required: true,
}, },
issuableTemplates: { issuableTemplates: {
type: Array, type: Object,
required: false, required: false,
default: () => [], default: () => {},
}, },
issuableType: { issuableType: {
type: String, type: String,
...@@ -46,6 +46,10 @@ export default { ...@@ -46,6 +46,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: Number,
required: true,
},
projectNamespace: { projectNamespace: {
type: String, type: String,
required: true, required: true,
...@@ -68,7 +72,7 @@ export default { ...@@ -68,7 +72,7 @@ export default {
}, },
computed: { computed: {
hasIssuableTemplates() { hasIssuableTemplates() {
return this.issuableTemplates.length; return Object.values(Object(this.issuableTemplates)).length;
}, },
showLockedWarning() { showLockedWarning() {
return this.formState.lockedWarningVisible && !this.formState.updateLoading; return this.formState.lockedWarningVisible && !this.formState.updateLoading;
...@@ -127,6 +131,7 @@ export default { ...@@ -127,6 +131,7 @@ export default {
:form-state="formState" :form-state="formState"
:issuable-templates="issuableTemplates" :issuable-templates="issuableTemplates"
:project-path="projectPath" :project-path="projectPath"
:project-id="projectId"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
/> />
</div> </div>
......
...@@ -54,6 +54,7 @@ export function initIssueHeaderActions(store) { ...@@ -54,6 +54,7 @@ export function initIssueHeaderActions(store) {
issueType: el.dataset.issueType, issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath, newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath, projectPath: el.dataset.projectPath,
projectId: el.dataset.projectId,
reportAbusePath: el.dataset.reportAbusePath, reportAbusePath: el.dataset.reportAbusePath,
submitAsSpamPath: el.dataset.submitAsSpamPath, submitAsSpamPath: el.dataset.submitAsSpamPath,
}, },
......
...@@ -11,7 +11,7 @@ export default class Store { ...@@ -11,7 +11,7 @@ export default class Store {
lockedWarningVisible: false, lockedWarningVisible: false,
updateLoading: false, updateLoading: false,
lock_version: 0, lock_version: 0,
issuableTemplates: [], issuableTemplates: {},
}; };
} }
......
...@@ -9,6 +9,7 @@ export default class IssuableTemplateSelector extends TemplateSelector { ...@@ -9,6 +9,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.projectId = this.dropdown.data('projectId');
this.projectPath = this.dropdown.data('projectPath'); this.projectPath = this.dropdown.data('projectPath');
this.namespacePath = this.dropdown.data('namespacePath'); this.namespacePath = this.dropdown.data('namespacePath');
this.issuableType = this.$dropdownContainer.data('issuableType'); this.issuableType = this.$dropdownContainer.data('issuableType');
...@@ -81,21 +82,21 @@ export default class IssuableTemplateSelector extends TemplateSelector { ...@@ -81,21 +82,21 @@ export default class IssuableTemplateSelector extends TemplateSelector {
} }
requestFile(query) { requestFile(query) {
this.startLoadingSpinner(); const callback = (currentTemplate) => {
Api.issueTemplate(
this.namespacePath,
this.projectPath,
query.name,
this.issuableType,
(err, currentTemplate) => {
this.currentTemplate = currentTemplate; this.currentTemplate = currentTemplate;
this.stopLoadingSpinner(); this.stopLoadingSpinner();
if (err) return; // Error handled by global AJAX error handler
this.setInputValueToTemplateContent(); this.setInputValueToTemplateContent();
}, };
this.startLoadingSpinner();
Api.projectTemplate(
this.projectId,
this.issuableType,
query.name,
{ source_template_project_id: query.project_id },
callback,
); );
return;
} }
setInputValueToTemplateContent() { setInputValueToTemplateContent() {
......
# frozen_string_literal: true # frozen_string_literal: true
class Projects::TemplatesController < Projects::ApplicationController class Projects::TemplatesController < Projects::ApplicationController
include IssuablesDescriptionTemplatesHelper
before_action :authenticate_user! before_action :authenticate_user!
before_action :authorize_can_read_issuable! before_action :authorize_can_read_issuable!
before_action :get_template_class before_action :get_template_class
...@@ -24,10 +26,8 @@ class Projects::TemplatesController < Projects::ApplicationController ...@@ -24,10 +26,8 @@ class Projects::TemplatesController < Projects::ApplicationController
end end
def names def names
templates = @template_type.dropdown_names(project)
respond_to do |format| respond_to do |format|
format.json { render json: templates } format.json { render json: issuable_templates(project, params[:template_type]) }
end end
end end
......
...@@ -36,6 +36,7 @@ class LicenseTemplateFinder ...@@ -36,6 +36,7 @@ class LicenseTemplateFinder
LicenseTemplate.new( LicenseTemplate.new(
key: license.key, key: license.key,
name: license.name, name: license.name,
project: project,
nickname: license.nickname, nickname: license.nickname,
category: (license.featured? ? :Popular : :Other), category: (license.featured? ? :Popular : :Other),
content: license.content, content: license.content,
......
...@@ -199,7 +199,7 @@ module BlobHelper ...@@ -199,7 +199,7 @@ module BlobHelper
categories.each_with_object({}) do |category, hash| categories.each_with_object({}) do |category, hash|
hash[category] = grouped[category].map do |item| hash[category] = grouped[category].map do |item|
{ name: item.name, id: item.key } { name: item.name, id: item.key, project_id: item.project_id }
end end
end end
end end
......
# frozen_string_literal: true
module IssuablesDescriptionTemplatesHelper
include Gitlab::Utils::StrongMemoize
include GitlabRoutingHelper
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
toggle_class: 'js-issuable-selector',
title: title,
filter: true,
placeholder: 'Filter',
footer_content: true,
data: {
data: issuable_templates(ref_project, issuable.to_ability_name),
field_name: 'issuable_template',
selected: selected_template(issuable),
project_id: ref_project.id,
project_path: ref_project.path,
namespace_path: ref_project.namespace.full_path
}
}
dropdown_tag(title, options: options) do
capture(&block)
end
end
def issuable_templates(project, issuable_type)
strong_memoize(:issuable_templates) do
supported_issuable_types = %w[issue merge_request]
next [] unless supported_issuable_types.include?(issuable_type)
template_dropdown_names(TemplateFinder.build(issuable_type.pluralize.to_sym, project).execute)
end
end
private
def issuable_templates_names(issuable)
issuable_templates(ref_project, issuable.to_ability_name).map { |template| template[:name] }
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(ref_project, issuable.to_ability_name).values.flatten.any? { |template| template[:name] == params[:issuable_template] }
end
def template_names_path(parent, issuable)
return '' unless parent.is_a?(Project)
project_template_names_path(parent, template_type: issuable.to_ability_name)
end
def template_dropdown_names(items)
grouped = items.group_by(&:category)
categories = grouped.keys
categories.each_with_object({}) do |category, hash|
hash[category] = grouped[category].map do |item|
{ name: item.name, id: item.key, project_id: item.try(:project_id) }
end
end
end
end
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
module IssuablesHelper module IssuablesHelper
include GitlabRoutingHelper include GitlabRoutingHelper
include IssuablesDescriptionTemplatesHelper
def sidebar_gutter_toggle_icon def sidebar_gutter_toggle_icon
content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do
...@@ -75,28 +76,6 @@ module IssuablesHelper ...@@ -75,28 +76,6 @@ module IssuablesHelper
.to_json .to_json
end end
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
toggle_class: 'js-issuable-selector',
title: title,
filter: true,
placeholder: 'Filter',
footer_content: true,
data: {
data: issuable_templates(issuable),
field_name: 'issuable_template',
selected: selected_template(issuable),
project_path: ref_project.path,
namespace_path: ref_project.namespace.full_path
}
}
dropdown_tag(title, options: options) do
capture(&block)
end
end
def users_dropdown_label(selected_users) def users_dropdown_label(selected_users)
case selected_users.length case selected_users.length
when 0 when 0
...@@ -294,6 +273,7 @@ module IssuablesHelper ...@@ -294,6 +273,7 @@ module IssuablesHelper
{ {
projectPath: ref_project.path, projectPath: ref_project.path,
projectId: ref_project.id,
projectNamespace: ref_project.namespace.full_path projectNamespace: ref_project.namespace.full_path
} }
end end
...@@ -369,24 +349,6 @@ module IssuablesHelper ...@@ -369,24 +349,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true' cookies[:collapsed_gutter] == 'true'
end end
def issuable_templates(issuable)
@issuable_templates ||=
case issuable
when Issue
ref_project.repository.issue_template_names
when MergeRequest
ref_project.repository.merge_request_template_names
end
end
def issuable_templates_names(issuable)
issuable_templates(issuable).map { |template| template[:name] }
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
def issuable_todo_button_data(issuable, is_collapsed) def issuable_todo_button_data(issuable, is_collapsed)
{ {
todo_text: _('Add a to do'), todo_text: _('Add a to do'),
...@@ -424,12 +386,6 @@ module IssuablesHelper ...@@ -424,12 +386,6 @@ module IssuablesHelper
end end
end end
def template_names_path(parent, issuable)
return '' unless parent.is_a?(Project)
project_template_names_path(parent, template_type: issuable.class.name.underscore)
end
def issuable_sidebar_options(issuable) def issuable_sidebar_options(issuable)
{ {
endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras", endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras",
......
...@@ -12,11 +12,12 @@ class LicenseTemplate ...@@ -12,11 +12,12 @@ class LicenseTemplate
(fullname|name\sof\s(author|copyright\sowner)) (fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]}xi.freeze [\>\}\]]}xi.freeze
attr_reader :key, :name, :category, :nickname, :url, :meta attr_reader :key, :name, :project, :category, :nickname, :url, :meta
def initialize(key:, name:, category:, content:, nickname: nil, url: nil, meta: {}) def initialize(key:, name:, project:, category:, content:, nickname: nil, url: nil, meta: {})
@key = key @key = key
@name = name @name = name
@project = project
@category = category @category = category
@content = content @content = content
@nickname = nickname @nickname = nickname
...@@ -24,6 +25,22 @@ class LicenseTemplate ...@@ -24,6 +25,22 @@ class LicenseTemplate
@meta = meta @meta = meta
end end
def project_id
project&.id
end
def project_path
project&.path
end
def namespace_id
project&.namespace&.id
end
def namespace_path
project&.namespace&.full_path
end
def popular? def popular?
category == :Popular category == :Popular
end end
......
- issuable = local_assigns.fetch(:issuable, nil) - issuable = local_assigns.fetch(:issuable, nil)
- return unless issuable && issuable_templates(issuable).any? - return unless issuable && issuable_templates(ref_project, issuable.class.name.underscore).any?
.issuable-form-select-holder.selectbox.form-group .issuable-form-select-holder.selectbox.form-group
.js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name } } .js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name.pluralize } }
= template_dropdown_tag(issuable) do = template_dropdown_tag(issuable) do
%ul.dropdown-footer-list %ul.dropdown-footer-list
%li %li
......
- issuable = local_assigns.fetch(:issuable) - issuable = local_assigns.fetch(:issuable)
- has_wip_commits = local_assigns.fetch(:has_wip_commits) - has_wip_commits = local_assigns.fetch(:has_wip_commits)
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
- no_issuable_templates = issuable_templates(issuable).empty? - no_issuable_templates = issuable_templates(ref_project, issuable.class.name.underscore).empty?
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8' - div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">' - toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
- toggle_wip_link_end = '</a>' - toggle_wip_link_end = '</a>'
......
...@@ -10,8 +10,8 @@ We have implemented standard features that depend on configuration files in the ...@@ -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: 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/`. - [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/`. - [Issue Templates](../user/project/description_templates.md#create-an-issue-template): `.gitlab/issue_templates/`.
- [Merge Request Templates](../user/project/description_templates.md#creating-merge-request-templates): `.gitlab/merge_request_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/`. - [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`. - [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`. - [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.
......
...@@ -52,7 +52,7 @@ With Maintainer or higher [permissions](../../user/permissions.md), you can enab ...@@ -52,7 +52,7 @@ With Maintainer or higher [permissions](../../user/permissions.md), you can enab
1. Navigate to **Settings > Operations > Incidents** and expand **Incidents**. 1. Navigate to **Settings > Operations > Incidents** and expand **Incidents**.
1. Check the **Create an incident** checkbox. 1. Check the **Create an incident** checkbox.
1. To customize the incident, select an 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](alert_notifications.md#email-notifications) to users 1. To send [an email notification](alert_notifications.md#email-notifications) to users
with [Developer permissions](../../user/permissions.md), select with [Developer permissions](../../user/permissions.md), select
**Send a separate email notification to Developers**. Email notifications are **Send a separate email notification to Developers**. Email notifications are
......
...@@ -726,6 +726,9 @@ To enable this feature, navigate to the group settings page, expand the ...@@ -726,6 +726,9 @@ To enable this feature, navigate to the group settings page, expand the
![Group file template settings](img/group_file_template_settings.png) ![Group file template settings](img/group_file_template_settings.png)
To learn how to create templates for issues and merge requests, visit
[Description templates](../project/description_templates.md).
#### Group-level project templates **(PREMIUM)** #### Group-level project templates **(PREMIUM)**
Define project templates at a group level by setting a group as the template source. Define project templates at a group level by setting a group as the template source.
......
...@@ -6,16 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -6,16 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Description templates # Description templates
>[Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4981) in GitLab 8.11. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4981) in GitLab 8.11.
We all know that a properly submitted issue is more likely to be addressed in We all know that a properly submitted issue is more likely to be addressed in
a timely manner by the developers of a project. a timely manner by the developers of a project.
Description templates allow you to define context-specific templates for issue With description templates, you can define context-specific templates for issue and merge request
and merge request description fields for your project, as well as help filter description fields for your project, and filter out a lot of unnecessary noise from issues.
out a lot of unnecessary noise from issues.
## Overview
By using the description templates, users that create a new issue or merge By using the description templates, users that create a new issue or merge
request can select a description template to help them communicate with other request can select a description template to help them communicate with other
...@@ -28,7 +25,10 @@ Description templates must be written in [Markdown](../markdown.md) and stored ...@@ -28,7 +25,10 @@ Description templates must be written in [Markdown](../markdown.md) and stored
in your project's repository under a directory named `.gitlab`. Only the in your project's repository under a directory named `.gitlab`. Only the
templates of the default branch are taken into account. templates of the default branch are taken into account.
## Use-cases To learn how to create templates for various file types in groups, visit
[Group file templates](../group/index.md#group-file-templates).
## Use cases
- Add a template to be used in every issue for a specific project, - Add a template to be used in every issue for a specific project,
giving instructions and guidelines, requiring for information specific to that subject. giving instructions and guidelines, requiring for information specific to that subject.
...@@ -40,7 +40,7 @@ templates of the default branch are taken into account. ...@@ -40,7 +40,7 @@ templates of the default branch are taken into account.
- You can also create issues and merge request templates for different - 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. stages of your workflow, for example, feature proposal, feature improvement, or a bug report.
## Creating issue templates ## Create an issue template
Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/` Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
directory in your repository. Commit and push to your default branch. directory in your repository. Commit and push to your default branch.
...@@ -65,13 +65,13 @@ To create the `.gitlab/issue_templates` directory: ...@@ -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) 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. 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 Similarly to issue templates, create a new Markdown (`.md`) file inside the
`.gitlab/merge_request_templates/` directory in your repository. Commit and `.gitlab/merge_request_templates/` directory in your repository. Commit and
push to your default branch. 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`. 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 This enables the `Bug` dropdown option when creating or editing issues. When
...@@ -80,15 +80,46 @@ to the issue description field. The **Reset template** button discards any ...@@ -80,15 +80,46 @@ to the issue description field. The **Reset template** button discards any
changes you made after picking the template and returns it to its initial status. changes you made after picking the template and returns it to its initial status.
NOTE: NOTE:
You can create short-cut links to create an issue using a designated template. For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`. You can create shortcut links to create an issue using a designated template.
For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
![Description templates](img/description_templates.png) ![Description templates](img/description_templates.png)
## Setting a default template for merge requests and issues **(STARTER)** ### Set an issue and merge request description template at group level **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46222) in GitLab 13.8.
Templates are most 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 your project'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/46222) in GitLab 13.8.
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. Select **Templates**.
1. From the dropdown, select your template project as the template repository at instance level.
> - This feature was introduced before [description templates](#overview) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings. Learn more about [instance template repository](../admin_area/settings/instance_template_repository.md).
> - Templates for issues were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.
> - Templates for merge requests were [introduced](https://gitlab.com/gitlab-org/gitlab/commit/7478ece8b48e80782b5465b96c79f85cc91d391b) in GitLab EE 6.9. ![Setting templates in the Admin Area](../admin_area/settings/img/file_template_admin_area.png)
### Set a default template for merge requests and issues **(STARTER)**
> - This feature was introduced before [description templates](#description-templates) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings.
> - Templates for issues [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.
> - Templates for merge requests [introduced](https://gitlab.com/gitlab-org/gitlab/commit/7478ece8b48e80782b5465b96c79f85cc91d391b) in GitLab EE 6.9.
The visibility of issues and/or merge requests should be set to either "Everyone The visibility of issues and/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 with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the
...@@ -113,52 +144,47 @@ pre-filled with the text you entered in the template(s). ...@@ -113,52 +144,47 @@ pre-filled with the text you entered in the template(s).
## Description template example ## Description template example
We make use of Description Templates for Issues and Merge Requests within the GitLab Community We make use of description templates for issues and merge requests in the GitLab project.
Edition project. Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab) Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab)
for some examples. for some examples.
NOTE: NOTE:
It's possible to use [quick actions](quick_actions.md) within description templates to quickly add It's possible to use [quick actions](quick_actions.md) in description templates to quickly add
labels, assignees, and milestones. The quick actions are only executed if the user submitting labels, assignees, and milestones. The quick actions are only executed if the user submitting
the issue or merge request has the permissions to perform the relevant actions. the issue or merge request has the permissions to perform the relevant actions.
Here is an example of a Bug report template: Here is an example of a Bug report template:
```plaintext ```markdown
Summary ## Summary
(Summarize the bug encountered concisely) (Summarize the bug encountered concisely)
## Steps to reproduce
Steps to reproduce
(How one can reproduce the issue - this is very important) (How one can reproduce the issue - this is very important)
## Example Project
Example Project (If possible, please create an example project here on GitLab.com that exhibits the problematic
behaviour, and link to it here in the bug report.
(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report) If you are using an older version of GitLab, this will also determine whether the bug has been fixed
in a more recent version)
(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version) ## What is the current bug behavior?
What is the current bug behavior?
(What actually happens) (What actually happens)
## What is the expected correct behavior?
What is the expected correct behavior?
(What you should see instead) (What you should see instead)
## Relevant logs and/or screenshots
Relevant logs and/or screenshots (Paste any relevant logs - please use code blocks (```) to format console output, logs, and code, as
it's very hard to read otherwise.)
(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)
Possible fixes ## Possible fixes
(If you can, link to the line of code that might be responsible for the problem) (If you can, link to the line of code that might be responsible for the problem)
......
...@@ -217,7 +217,7 @@ You can then see issue statuses in the [issue list](#issues-list) and the ...@@ -217,7 +217,7 @@ You can then see issue statuses in the [issue list](#issues-list) and the
## Other Issue actions ## 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) - [Set a due date](due_dates.md)
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues - [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. in order to change their status, assignee, milestone, or labels in bulk.
......
...@@ -102,7 +102,7 @@ To edit a file: ...@@ -102,7 +102,7 @@ To edit a file:
in the bottom-right corner. in the bottom-right corner.
1. When you're done, click **Submit changes...**. 1. When you're done, click **Submit changes...**.
1. (Optional) Adjust the default title and description of the merge request that will be submitted 1. (Optional) Adjust the default title and description of the merge request that will be submitted
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. from the dropdown menu and edit it accordingly.
1. Click **Submit changes**. 1. Click **Submit changes**.
1. A new merge request is automatically created and you can assign a colleague for review. 1. A new merge request is automatically created and you can assign a colleague for review.
......
...@@ -45,6 +45,7 @@ export default { ...@@ -45,6 +45,7 @@ export default {
:endpoint="endpoint" :endpoint="endpoint"
:update-endpoint="updateEndpoint" :update-endpoint="updateEndpoint"
:project-path="groupPath" :project-path="groupPath"
:project-id="0"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:can-update="canUpdate" :can-update="canUpdate"
......
...@@ -19,7 +19,7 @@ module EE ...@@ -19,7 +19,7 @@ module EE
return super unless custom_templates? return super unless custom_templates?
if params[:name] if params[:name]
custom_templates.find(params[:name]) || super custom_templates.find(params[:name], params[:source_template_project_id]) || super
else else
custom_templates.all + super custom_templates.all + super
end end
......
...@@ -8,7 +8,9 @@ module EE ...@@ -8,7 +8,9 @@ module EE
dockerfiles: ::Gitlab::Template::CustomDockerfileTemplate, dockerfiles: ::Gitlab::Template::CustomDockerfileTemplate,
gitignores: ::Gitlab::Template::CustomGitignoreTemplate, gitignores: ::Gitlab::Template::CustomGitignoreTemplate,
gitlab_ci_ymls: ::Gitlab::Template::CustomGitlabCiYmlTemplate, gitlab_ci_ymls: ::Gitlab::Template::CustomGitlabCiYmlTemplate,
metrics_dashboard_ymls: ::Gitlab::Template::CustomMetricsDashboardYmlTemplate metrics_dashboard_ymls: ::Gitlab::Template::CustomMetricsDashboardYmlTemplate,
issues: ::Gitlab::Template::IssueTemplate,
merge_requests: ::Gitlab::Template::MergeRequestTemplate
).freeze ).freeze
attr_reader :custom_templates attr_reader :custom_templates
...@@ -28,7 +30,7 @@ module EE ...@@ -28,7 +30,7 @@ module EE
return super if custom_templates.nil? || !custom_templates.enabled? return super if custom_templates.nil? || !custom_templates.enabled?
if params[:name] if params[:name]
custom_templates.find(params[:name]) || super custom_templates.find(params[:name], params[:source_template_project_id]) || super
else else
custom_templates.all + super custom_templates.all + super
end end
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.settings-header .settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Default description template for issues') %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= expanded ? _('Collapse') : _('Expand') %button.gl-button.btn.btn-default.js-settings-toggle= 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 } %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 .settings-content
......
---
title: Configure issue and merge request description templates at group level and
rolldown description templates in the group hierarchy
merge_request: 46222
author:
type: changed
...@@ -30,8 +30,10 @@ module Gitlab ...@@ -30,8 +30,10 @@ module Gitlab
by_namespace + by_instance by_namespace + by_instance
end end
def find(name) def find(name, source_template_project_id = nil)
namespace_template_projects_hash.each do |namespace, project| namespace_template_projects_hash.each do |namespace, project|
next if source_template_project_id && project.id != source_template_project_id.to_i
found = template_for(project, name, category_for(namespace)) found = template_for(project, name, category_for(namespace))
return found if found return found if found
end end
...@@ -79,18 +81,18 @@ module Gitlab ...@@ -79,18 +81,18 @@ module Gitlab
def templates_for(project, category) def templates_for(project, category)
return [] unless project return [] unless project
finder.all(project).map { |template| translate(template, category: category) } finder.all(project).map { |template| translate(template, project, category: category) }
end end
def template_for(project, name, category) def template_for(project, name, category)
return unless project return unless project
translate(finder.find(name, project), category: category) translate(finder.find(name, project), project, category: category)
rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
nil nil
end end
def translate(template, category:) def translate(template, project, category:)
return unless template return unless template
template.category = category template.category = category
...@@ -103,6 +105,7 @@ module Gitlab ...@@ -103,6 +105,7 @@ module Gitlab
LicenseTemplate.new( LicenseTemplate.new(
key: template.key, key: template.key,
name: template.name, name: template.name,
project: project,
nickname: template.name, nickname: template.name,
category: template.category, category: template.category,
content: -> { template.content } content: -> { template.content }
......
...@@ -7,7 +7,7 @@ RSpec.describe LicenseTemplateFinder do ...@@ -7,7 +7,7 @@ RSpec.describe LicenseTemplateFinder do
let(:params) { {} } let(:params) { {} }
let(:fake_template_source) { double(::Gitlab::CustomFileTemplates) } let(:fake_template_source) { double(::Gitlab::CustomFileTemplates) }
let(:custom_template) { ::LicenseTemplate.new(key: 'foo', name: 'foo', category: nil, content: 'Template') } let(:custom_template) { ::LicenseTemplate.new(key: 'foo', name: 'foo', project: project, category: nil, content: 'Template') }
let(:custom_templates) { [custom_template] } let(:custom_templates) { [custom_template] }
subject(:finder) { described_class.new(project, params) } subject(:finder) { described_class.new(project, params) }
...@@ -23,7 +23,7 @@ RSpec.describe LicenseTemplateFinder do ...@@ -23,7 +23,7 @@ RSpec.describe LicenseTemplateFinder do
allow(fake_template_source) allow(fake_template_source)
.to receive(:find) .to receive(:find)
.with(custom_template.key) .with(custom_template.key, nil)
.and_return(custom_template) .and_return(custom_template)
allow(fake_template_source) allow(fake_template_source)
......
...@@ -20,6 +20,8 @@ RSpec.describe TemplateFinder do ...@@ -20,6 +20,8 @@ RSpec.describe TemplateFinder do
:dockerfiles | ::Gitlab::Template::CustomDockerfileTemplate :dockerfiles | ::Gitlab::Template::CustomDockerfileTemplate
:gitignores | ::Gitlab::Template::CustomGitignoreTemplate :gitignores | ::Gitlab::Template::CustomGitignoreTemplate
:gitlab_ci_ymls | ::Gitlab::Template::CustomGitlabCiYmlTemplate :gitlab_ci_ymls | ::Gitlab::Template::CustomGitlabCiYmlTemplate
:issues | ::Gitlab::Template::IssueTemplate
:merge_requests | ::Gitlab::Template::MergeRequestTemplate
end end
with_them do with_them do
...@@ -33,7 +35,7 @@ RSpec.describe TemplateFinder do ...@@ -33,7 +35,7 @@ RSpec.describe TemplateFinder do
allow(fake_template_source) allow(fake_template_source)
.to receive(:find) .to receive(:find)
.with(custom_template.key) .with(custom_template.key, nil)
.and_return(custom_template) .and_return(custom_template)
allow(fake_template_source) allow(fake_template_source)
......
...@@ -32,7 +32,7 @@ RSpec.describe BlobHelper do ...@@ -32,7 +32,7 @@ RSpec.describe BlobHelper do
.and_return([OpenStruct.new(key: 'name', name: 'Name')]) .and_return([OpenStruct.new(key: 'name', name: 'Name')])
expect(categories).to contain_exactly(:Popular, :Other, group_category) expect(categories).to contain_exactly(:Popular, :Other, group_category)
expect(by_group).to contain_exactly({ id: 'name', name: 'Name' }) expect(by_group).to contain_exactly({ id: 'name', name: 'Name', project_id: project.id })
expect(by_popular).to be_present expect(by_popular).to be_present
expect(by_other).to be_present expect(by_other).to be_present
end end
...@@ -46,7 +46,7 @@ RSpec.describe BlobHelper do ...@@ -46,7 +46,7 @@ RSpec.describe BlobHelper do
.and_return([OpenStruct.new(key: 'name', name: 'Name')]) .and_return([OpenStruct.new(key: 'name', name: 'Name')])
expect(categories).to contain_exactly(:Popular, :Other, 'Instance') expect(categories).to contain_exactly(:Popular, :Other, 'Instance')
expect(by_instance).to contain_exactly({ id: 'name', name: 'Name' }) expect(by_instance).to contain_exactly({ id: 'name', name: 'Name', project_id: project.id })
expect(by_popular).to be_present expect(by_popular).to be_present
expect(by_other).to be_present expect(by_other).to be_present
end end
......
# 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(:parent_group) { create(:group) }
let_it_be(:group) { create(:group, parent: parent_group) }
let_it_be(: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 ...@@ -27,20 +27,34 @@ RSpec.describe "Custom file template classes" do
'Dockerfile/category/baz.txt' => 'CustomDockerfileTemplate category baz', 'Dockerfile/category/baz.txt' => 'CustomDockerfileTemplate category baz',
'gitignore/category/baz.txt' => 'CustomGitignoreTemplate category baz', 'gitignore/category/baz.txt' => 'CustomGitignoreTemplate category baz',
'gitlab-ci/category/baz.yml' => 'CustomGitlabCiYmlTemplate 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(:project) { create(:project, :custom_repo, files: files) } let(:project) { create(:project, :custom_repo, files: files) }
[ custom_templates = [
::Gitlab::Template::CustomDockerfileTemplate, { class_name: ::Gitlab::Template::CustomDockerfileTemplate, category: 'Custom' },
::Gitlab::Template::CustomGitignoreTemplate, { class_name: ::Gitlab::Template::CustomGitignoreTemplate, category: 'Custom' },
::Gitlab::Template::CustomGitlabCiYmlTemplate, { class_name: ::Gitlab::Template::CustomGitlabCiYmlTemplate, category: 'Custom' },
::Gitlab::Template::CustomLicenseTemplate, { class_name: ::Gitlab::Template::CustomLicenseTemplate, category: 'Custom' },
::Gitlab::Template::CustomMetricsDashboardYmlTemplate { class_name: ::Gitlab::Template::CustomMetricsDashboardYmlTemplate, category: 'Custom' },
].each do |template_class| { class_name: ::Gitlab::Template::IssueTemplate, category: 'Project Templates' },
describe template_class do { class_name: ::Gitlab::Template::MergeRequestTemplate, category: 'Project Templates' }
let(:name) { template_class.name.demodulize } ].freeze
custom_templates.each do |template_class|
describe template_class[:class_name] do
let(:name) { template_class[:class_name].name.demodulize }
describe '.all' do describe '.all' do
it 'returns all valid templates' do it 'returns all valid templates' do
...@@ -48,7 +62,7 @@ RSpec.describe "Custom file template classes" do ...@@ -48,7 +62,7 @@ RSpec.describe "Custom file template classes" do
aggregate_failures do aggregate_failures do
expect(found.map(&:name)).to contain_exactly('foo', 'bar') 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 end
end end
......
...@@ -45,9 +45,10 @@ module API ...@@ -45,9 +45,10 @@ module API
get ':id/templates/:type/:name', requirements: TEMPLATE_NAMES_ENDPOINT_REQUIREMENTS do get ':id/templates/:type/:name', requirements: TEMPLATE_NAMES_ENDPOINT_REQUIREMENTS do
begin begin
template = TemplateFinder template = TemplateFinder.build(
.build(params[:type], user_project, name: params[:name]) params[:type], user_project, name: params[:name],
.execute source_template_project_id: params[:source_template_project_id]
).execute
rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
not_found!('Template') not_found!('Template')
end end
......
...@@ -8,6 +8,7 @@ module Gitlab ...@@ -8,6 +8,7 @@ module Gitlab
def initialize(path, project = nil, category: nil) def initialize(path, project = nil, category: nil)
@path = path @path = path
@category = category @category = category
@project = project
@finder = self.class.finder(project) @finder = self.class.finder(project)
end end
...@@ -31,6 +32,22 @@ module Gitlab ...@@ -31,6 +32,22 @@ module Gitlab
# override with a comment to be placed at the top of the blob. # override with a comment to be placed at the top of the blob.
end end
def project_id
@project&.id
end
def project_path
@project&.path
end
def namespace_id
@project&.namespace&.id
end
def namespace_path
@project&.namespace&.full_path
end
# Present for compatibility with license templates, which can replace text # Present for compatibility with license templates, which can replace text
# like `[fullname]` with a user-specified string. This is a no-op for # like `[fullname]` with a user-specified string. This is a no-op for
# other templates # other templates
...@@ -82,11 +99,11 @@ module Gitlab ...@@ -82,11 +99,11 @@ module Gitlab
raise NotImplementedError raise NotImplementedError
end end
def by_category(category, project = nil) def by_category(category, project = nil, empty_category_title: nil)
directory = category_directory(category) directory = category_directory(category)
files = finder(project).list_files_for(directory) 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 end
def category_directory(category) def category_directory(category)
......
...@@ -15,6 +15,10 @@ module Gitlab ...@@ -15,6 +15,10 @@ module Gitlab
def finder(project) def finder(project)
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories) Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
end end
def by_category(category, project = nil, empty_category_title: nil)
super(category, project, empty_category_title: _('Project Templates'))
end
end end
end end
end end
......
...@@ -15,6 +15,10 @@ module Gitlab ...@@ -15,6 +15,10 @@ module Gitlab
def finder(project) def finder(project)
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories) Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
end end
def by_category(category, project = nil, empty_category_title: nil)
super(category, project, empty_category_title: _('Project Templates'))
end
end end
end end
end end
......
...@@ -21882,6 +21882,9 @@ msgstr "" ...@@ -21882,6 +21882,9 @@ msgstr ""
msgid "Project ID" msgid "Project ID"
msgstr "" msgstr ""
msgid "Project Templates"
msgstr ""
msgid "Project URL" msgid "Project URL"
msgstr "" msgstr ""
......
...@@ -160,12 +160,12 @@ RSpec.describe Projects::TemplatesController do ...@@ -160,12 +160,12 @@ RSpec.describe Projects::TemplatesController do
end end
shared_examples 'template names request' do shared_examples 'template names request' do
it 'returns the template names' 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) 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(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2) expect(json_response['Project Templates'].size).to eq(2)
expect(json_response).to match(expected_template_names) expect(json_response['Project Templates'].map { |x| { "name" => x['name'] } }).to match(expected_template_names)
end end
it 'fails for user with no access' do it 'fails for user with no access' do
......
...@@ -35,7 +35,7 @@ exports[`Alert integration settings form default state should match the default ...@@ -35,7 +35,7 @@ exports[`Alert integration settings form default state should match the default
Incident template (optional) Incident template (optional)
<gl-link-stub <gl-link-stub
href="/help/user/project/description_templates#creating-issue-templates" href="/help/user/project/description_templates#create-an-issue-template"
target="_blank" target="_blank"
> >
<gl-icon-stub <gl-icon-stub
......
...@@ -423,7 +423,9 @@ describe('Issuable output', () => { ...@@ -423,7 +423,9 @@ describe('Issuable output', () => {
}); });
it('shows the form if template names request is successful', () => { it('shows the form if template names request is successful', () => {
const mockData = [{ name: 'Bug' }]; const mockData = {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
};
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData])); mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => { return wrapper.vm.requestTemplatesAndShowForm().then(() => {
......
...@@ -14,7 +14,10 @@ describe('Issue description template component', () => { ...@@ -14,7 +14,10 @@ describe('Issue description template component', () => {
vm = new Component({ vm = new Component({
propsData: { propsData: {
formState, formState,
issuableTemplates: [{ name: 'test' }], issuableTemplates: {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
},
projectId: 1,
projectPath: '/', projectPath: '/',
projectNamespace: '/', projectNamespace: '/',
}, },
...@@ -23,7 +26,7 @@ describe('Issue description template component', () => { ...@@ -23,7 +26,7 @@ describe('Issue description template component', () => {
it('renders templates as JSON array in data attribute', () => { it('renders templates as JSON array in data attribute', () => {
expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe( expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
'[{"name":"test"}]', '{"test":[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]}',
); );
}); });
......
...@@ -19,6 +19,7 @@ describe('Inline edit form component', () => { ...@@ -19,6 +19,7 @@ describe('Inline edit form component', () => {
markdownPreviewPath: '/', markdownPreviewPath: '/',
markdownDocsPath: '/', markdownDocsPath: '/',
projectPath: '/', projectPath: '/',
projectId: 1,
projectNamespace: '/', projectNamespace: '/',
}; };
...@@ -42,7 +43,11 @@ describe('Inline edit form component', () => { ...@@ -42,7 +43,11 @@ describe('Inline edit form component', () => {
}); });
it('renders template selector when templates exists', () => { it('renders template selector when templates exists', () => {
createComponent({ issuableTemplates: ['test'] }); createComponent({
issuableTemplates: {
test: [{ name: 'test', id: 'test', project_path: 'test', namespace_path: 'test' }],
},
});
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull(); expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
}); });
......
...@@ -52,6 +52,7 @@ export const appProps = { ...@@ -52,6 +52,7 @@ export const appProps = {
markdownDocsPath: '/', markdownDocsPath: '/',
projectNamespace: '/', projectNamespace: '/',
projectPath: '/', projectPath: '/',
projectId: 1,
issuableTemplateNamesPath: '/issuable-templates-path', issuableTemplateNamesPath: '/issuable-templates-path',
zoomMeetingUrl, zoomMeetingUrl,
publishedIncidentUrl, publishedIncidentUrl,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssuablesDescriptionTemplatesHelper do
include_context 'project issuable templates context'
describe '#issuable_templates' do
let_it_be(:inherited_from) { nil }
let_it_be(:user) { create(:user) }
let_it_be(:parent_group) { create(:group) }
let_it_be(:project) { create(:project, :custom_repo, files: issuable_template_files) }
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) }
context 'when project has no parent group' do
it_behaves_like 'project issuable templates'
end
context 'when project has parent group' do
before do
project.update!(group: parent_group)
end
context 'when project parent group does not have a file template project' do
it_behaves_like 'project issuable templates'
end
context 'when project parent group has a file template project' do
let_it_be(:file_template_project) { create(:project, :custom_repo, group: parent_group, files: issuable_template_files) }
let_it_be(:group) { create(:group, parent: parent_group) }
let_it_be(:project) { create(:project, :custom_repo, group: group, files: issuable_template_files) }
before do
project.update!(group: group)
parent_group.update_columns(file_template_project_id: file_template_project.id)
end
it_behaves_like 'project issuable templates'
end
end
end
end
...@@ -199,6 +199,7 @@ RSpec.describe IssuablesHelper do ...@@ -199,6 +199,7 @@ RSpec.describe IssuablesHelper do
markdownDocsPath: '/help/user/markdown', markdownDocsPath: '/help/user/markdown',
lockVersion: issue.lock_version, lockVersion: issue.lock_version,
projectPath: @project.path, projectPath: @project.path,
projectId: @project.id,
projectNamespace: @project.namespace.path, projectNamespace: @project.namespace.path,
initialTitleHtml: issue.title, initialTitleHtml: issue.title,
initialTitleText: issue.title, initialTitleText: issue.title,
......
...@@ -57,6 +57,6 @@ RSpec.describe LicenseTemplate do ...@@ -57,6 +57,6 @@ RSpec.describe LicenseTemplate do
end end
def build_template(content) def build_template(content)
described_class.new(key: 'foo', name: 'foo', category: :Other, content: content) described_class.new(key: 'foo', name: 'foo', project: nil, category: :Other, content: content)
end end
end end
# frozen_string_literal: true
RSpec.shared_context 'project issuable templates context' do
let_it_be(:issuable_template_files) do
{
'.gitlab/issue_templates/issue-bar.md' => 'Issue Template Bar',
'.gitlab/issue_templates/issue-foo.md' => 'Issue Template Foo',
'.gitlab/issue_templates/issue-bad.txt' => 'Issue Template Bad',
'.gitlab/issue_templates/issue-baz.xyz' => 'Issue Template Baz',
'.gitlab/merge_request_templates/merge_request-bar.md' => 'Merge Request Template Bar',
'.gitlab/merge_request_templates/merge_request-foo.md' => 'Merge Request Template Foo',
'.gitlab/merge_request_templates/merge_request-bad.txt' => 'Merge Request Template Bad',
'.gitlab/merge_request_templates/merge_request-baz.xyz' => 'Merge Request Template Baz'
}
end
end
RSpec.shared_examples 'project issuable templates' do
context 'issuable templates' do
before do
allow(helper).to receive(:current_user).and_return(user)
end
it 'returns only md files as issue templates' do
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(expected_templates('merge_request'))
end
end
def expected_templates(issuable_type)
expectation = {}
expectation["Project Templates"] = templates(issuable_type, project)
expectation["Group #{inherited_from.namespace.full_name}"] = templates(issuable_type, inherited_from) if inherited_from.present?
expectation
end
def templates(issuable_type, inherited_from)
[
{ id: "#{issuable_type}-bar", name: "#{issuable_type}-bar", project_id: inherited_from.id },
{ id: "#{issuable_type}-foo", name: "#{issuable_type}-foo", project_id: inherited_from.id }
]
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