* Issue templates Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add some comments, appease the linter Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add docs and re-use dir candidates Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add default labels to issue templates Signed-off-by: jolheiser <john.olheiser@gmail.com> * Generate swagger Signed-off-by: jolheiser <john.olheiser@gmail.com> * Suggested changes Signed-off-by: jolheiser <john.olheiser@gmail.com> * Update issue.go * Suggestions Signed-off-by: jolheiser <john.olheiser@gmail.com> * Extract metadata from legacy if possible Signed-off-by: jolheiser <john.olheiser@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>tags/v1.13.0-rc1
@@ -41,4 +41,39 @@ Possible file names for PR templates: | |||
* .github/pull_request_template.md | |||
Additionally, the New Issue page URL can be suffixed with `?body=Issue+Text` and the form will be populated with that string. This string will be used instead of the template if there is one. | |||
Additionally, the New Issue page URL can be suffixed with `?title=Issue+Title&body=Issue+Text` and the form will be populated with those strings. Those strings will be used instead of the template if there is one. | |||
# Issue Template Directory | |||
Alternatively, users can create multiple issue templates inside a special directory and allow users to choose one that more specifically | |||
addresses their problem. | |||
Possible directory names for issue templates: | |||
* ISSUE_TEMPLATE | |||
* issue_template | |||
* .gitea/ISSUE_TEMPLATE | |||
* .gitea/issue_template | |||
* .github/ISSUE_TEMPLATE | |||
* .github/issue_template | |||
* .gitlab/ISSUE_TEMPLATE | |||
* .gitlab/issue_template | |||
Inside the directory can be multiple issue templates with the form | |||
```markdown | |||
----- | |||
name: "Template Name" | |||
about: "This template is for testing!" | |||
title: "[TEST] " | |||
labels: | |||
- bug | |||
- "help needed" | |||
----- | |||
This is the template! | |||
``` | |||
In the above example, when a user is presented with the list of issues they can submit, this would show as `Template Name` with the description | |||
`This template is for testing!`. When submitting an issue with the above example, the issue title would be pre-populated with | |||
`[TEST] ` while the issue body would be pre-populated with `This is the template!`. The issue would also be assigned two labels, | |||
`bug` and `help needed`. |
@@ -16,13 +16,27 @@ import ( | |||
"code.gitea.io/gitea/modules/cache" | |||
"code.gitea.io/gitea/modules/git" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/markup/markdown" | |||
"code.gitea.io/gitea/modules/setting" | |||
api "code.gitea.io/gitea/modules/structs" | |||
"gitea.com/macaron/macaron" | |||
"github.com/editorconfig/editorconfig-core-go/v2" | |||
"github.com/unknwon/com" | |||
) | |||
// IssueTemplateDirCandidates issue templates directory | |||
var IssueTemplateDirCandidates = []string{ | |||
"ISSUE_TEMPLATE", | |||
"issue_template", | |||
".gitea/ISSUE_TEMPLATE", | |||
".gitea/issue_template", | |||
".github/ISSUE_TEMPLATE", | |||
".github/issue_template", | |||
".gitlab/ISSUE_TEMPLATE", | |||
".gitlab/issue_template", | |||
} | |||
// PullRequest contains informations to make a pull request | |||
type PullRequest struct { | |||
BaseRepo *models.Repository | |||
@@ -821,3 +835,60 @@ func UnitTypes() macaron.Handler { | |||
ctx.Data["UnitTypeProjects"] = models.UnitTypeProjects | |||
} | |||
} | |||
// IssueTemplatesFromDefaultBranch checks for issue templates in the repo's default branch | |||
func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate { | |||
var issueTemplates []api.IssueTemplate | |||
if ctx.Repo.Commit == nil { | |||
var err error | |||
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) | |||
if err != nil { | |||
return issueTemplates | |||
} | |||
} | |||
for _, dirName := range IssueTemplateDirCandidates { | |||
tree, err := ctx.Repo.Commit.SubTree(dirName) | |||
if err != nil { | |||
continue | |||
} | |||
entries, err := tree.ListEntries() | |||
if err != nil { | |||
return issueTemplates | |||
} | |||
for _, entry := range entries { | |||
if strings.HasSuffix(entry.Name(), ".md") { | |||
if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize { | |||
log.Debug("Issue template is too large: %s", entry.Name()) | |||
continue | |||
} | |||
r, err := entry.Blob().DataAsync() | |||
if err != nil { | |||
log.Debug("DataAsync: %v", err) | |||
continue | |||
} | |||
defer r.Close() | |||
data, err := ioutil.ReadAll(r) | |||
if err != nil { | |||
log.Debug("ReadAll: %v", err) | |||
continue | |||
} | |||
var it api.IssueTemplate | |||
content, err := markdown.ExtractMetadata(string(data), &it) | |||
if err != nil { | |||
log.Debug("ExtractMetadata: %v", err) | |||
continue | |||
} | |||
it.Content = content | |||
it.FileName = entry.Name() | |||
if it.Valid() { | |||
issueTemplates = append(issueTemplates, it) | |||
} | |||
} | |||
} | |||
if len(issueTemplates) > 0 { | |||
return issueTemplates | |||
} | |||
} | |||
return issueTemplates | |||
} |
@@ -0,0 +1,49 @@ | |||
// Copyright 2020 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package markdown | |||
import ( | |||
"errors" | |||
"strings" | |||
"gopkg.in/yaml.v2" | |||
) | |||
func isYAMLSeparator(line string) bool { | |||
line = strings.TrimSpace(line) | |||
for i := 0; i < len(line); i++ { | |||
if line[i] != '-' { | |||
return false | |||
} | |||
} | |||
return len(line) > 2 | |||
} | |||
// ExtractMetadata consumes a markdown file, parses YAML frontmatter, | |||
// and returns the frontmatter metadata separated from the markdown content | |||
func ExtractMetadata(contents string, out interface{}) (string, error) { | |||
var front, body []string | |||
var seps int | |||
lines := strings.Split(contents, "\n") | |||
for idx, line := range lines { | |||
if seps == 2 { | |||
front, body = lines[:idx], lines[idx:] | |||
break | |||
} | |||
if isYAMLSeparator(line) { | |||
seps++ | |||
continue | |||
} | |||
} | |||
if len(front) == 0 && len(body) == 0 { | |||
return "", errors.New("could not determine metadata") | |||
} | |||
if err := yaml.Unmarshal([]byte(strings.Join(front, "\n")), out); err != nil { | |||
return "", err | |||
} | |||
return strings.Join(body, "\n"), nil | |||
} |
@@ -5,6 +5,7 @@ | |||
package structs | |||
import ( | |||
"strings" | |||
"time" | |||
) | |||
@@ -119,3 +120,19 @@ type IssueDeadline struct { | |||
// swagger:strfmt date-time | |||
Deadline *time.Time `json:"due_date"` | |||
} | |||
// IssueTemplate represents an issue template for a repository | |||
// swagger:model | |||
type IssueTemplate struct { | |||
Name string `json:"name" yaml:"name"` | |||
Title string `json:"title" yaml:"title"` | |||
About string `json:"about" yaml:"about"` | |||
Labels []string `json:"labels" yaml:"labels"` | |||
Content string `json:"content" yaml:"-"` | |||
FileName string `json:"file_name" yaml:"-"` | |||
} | |||
// Valid checks whether an IssueTemplate is considered valid, e.g. at least name and about | |||
func (it IssueTemplate) Valid() bool { | |||
return strings.TrimSpace(it.Name) != "" && strings.TrimSpace(it.About) != "" | |||
} |
@@ -939,6 +939,8 @@ issues.new.clear_assignees = Clear assignees | |||
issues.new.no_assignees = No Assignees | |||
issues.new.no_reviewers = No reviewers | |||
issues.new.add_reviewer_title = Request review | |||
issues.choose.get_started = Get Started | |||
issues.choose.blank = Open a blank issue | |||
issues.no_ref = No Branch/Tag Specified | |||
issues.create = Create Issue | |||
issues.new_label = New Label | |||
@@ -866,6 +866,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
Delete(reqToken(), repo.DeleteTopic) | |||
}, reqAdmin()) | |||
}, reqAnyRepoReader()) | |||
m.Get("/issue_templates", context.ReferencesGitRepo(false), repo.GetIssueTemplates) | |||
m.Get("/languages", reqRepoReader(models.UnitTypeCode), repo.GetLanguages) | |||
}, repoAssignment()) | |||
}) | |||
@@ -812,3 +812,28 @@ func Delete(ctx *context.APIContext) { | |||
log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name) | |||
ctx.Status(http.StatusNoContent) | |||
} | |||
// GetIssueTemplates returns the issue templates for a repository | |||
func GetIssueTemplates(ctx *context.APIContext) { | |||
// swagger:operation GET /repos/{owner}/{repo}/issue_templates repository repoGetIssueTemplates | |||
// --- | |||
// summary: Get available issue templates for a repository | |||
// produces: | |||
// - application/json | |||
// parameters: | |||
// - name: owner | |||
// in: path | |||
// description: owner of the repo | |||
// type: string | |||
// required: true | |||
// - name: repo | |||
// in: path | |||
// description: name of the repo | |||
// type: string | |||
// required: true | |||
// responses: | |||
// "200": | |||
// "$ref": "#/responses/IssueTemplates" | |||
ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch()) | |||
} |
@@ -85,6 +85,13 @@ type swaggerIssueDeadline struct { | |||
Body api.IssueDeadline `json:"body"` | |||
} | |||
// IssueTemplates | |||
// swagger:response IssueTemplates | |||
type swaggerIssueTemplates struct { | |||
// in:body | |||
Body []api.IssueTemplate `json:"body"` | |||
} | |||
// StopWatch | |||
// swagger:response StopWatch | |||
type swaggerResponseStopWatch struct { | |||
@@ -577,7 +577,7 @@ func CompareDiff(ctx *context.Context) { | |||
ctx.Data["RequireTribute"] = true | |||
ctx.Data["RequireSimpleMDE"] = true | |||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes | |||
setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates) | |||
setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates) | |||
renderAttachmentSettings(ctx) | |||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypePullRequests) | |||
@@ -11,6 +11,7 @@ import ( | |||
"fmt" | |||
"io/ioutil" | |||
"net/http" | |||
"path" | |||
"strconv" | |||
"strings" | |||
@@ -36,13 +37,15 @@ import ( | |||
const ( | |||
tplAttachment base.TplName = "repo/issue/view_content/attachments" | |||
tplIssues base.TplName = "repo/issue/list" | |||
tplIssueNew base.TplName = "repo/issue/new" | |||
tplIssueView base.TplName = "repo/issue/view" | |||
tplIssues base.TplName = "repo/issue/list" | |||
tplIssueNew base.TplName = "repo/issue/new" | |||
tplIssueChoose base.TplName = "repo/issue/choose" | |||
tplIssueView base.TplName = "repo/issue/view" | |||
tplReactions base.TplName = "repo/issue/view_content/reactions" | |||
issueTemplateKey = "IssueTemplate" | |||
issueTemplateKey = "IssueTemplate" | |||
issueTemplateTitleKey = "IssueTemplateTitle" | |||
) | |||
var ( | |||
@@ -356,6 +359,7 @@ func Issues(ctx *context.Context) { | |||
} | |||
ctx.Data["Title"] = ctx.Tr("repo.issues") | |||
ctx.Data["PageIsIssueList"] = true | |||
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 | |||
} | |||
issues(ctx, ctx.QueryInt64("milestone"), ctx.QueryInt64("project"), util.OptionalBoolOf(isPullList)) | |||
@@ -515,11 +519,41 @@ func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (str | |||
return string(bytes), true | |||
} | |||
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) { | |||
for _, filename := range possibleFiles { | |||
content, found := getFileContentFromDefaultBranch(ctx, filename) | |||
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs []string, possibleFiles []string) { | |||
templateCandidates := make([]string, 0, len(possibleFiles)) | |||
if ctx.Query("template") != "" { | |||
for _, dirName := range possibleDirs { | |||
templateCandidates = append(templateCandidates, path.Join(dirName, ctx.Query("template"))) | |||
} | |||
} | |||
templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback | |||
for _, filename := range templateCandidates { | |||
templateContent, found := getFileContentFromDefaultBranch(ctx, filename) | |||
if found { | |||
ctx.Data[ctxDataKey] = content | |||
var meta api.IssueTemplate | |||
templateBody, err := markdown.ExtractMetadata(templateContent, &meta) | |||
if err != nil { | |||
log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err) | |||
ctx.Data[ctxDataKey] = templateContent | |||
return | |||
} | |||
ctx.Data[issueTemplateTitleKey] = meta.Title | |||
ctx.Data[ctxDataKey] = templateBody | |||
labelIDs := make([]string, 0, len(meta.Labels)) | |||
if repoLabels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, "", models.ListOptions{}); err == nil { | |||
for _, metaLabel := range meta.Labels { | |||
for _, repoLabel := range repoLabels { | |||
if strings.EqualFold(repoLabel.Name, metaLabel) { | |||
repoLabel.IsChecked = true | |||
labelIDs = append(labelIDs, fmt.Sprintf("%d", repoLabel.ID)) | |||
break | |||
} | |||
} | |||
} | |||
ctx.Data["Labels"] = repoLabels | |||
} | |||
ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0 | |||
ctx.Data["label_ids"] = strings.Join(labelIDs, ",") | |||
return | |||
} | |||
} | |||
@@ -529,10 +563,13 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles | |||
func NewIssue(ctx *context.Context) { | |||
ctx.Data["Title"] = ctx.Tr("repo.issues.new") | |||
ctx.Data["PageIsIssueList"] = true | |||
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 | |||
ctx.Data["RequireHighlightJS"] = true | |||
ctx.Data["RequireSimpleMDE"] = true | |||
ctx.Data["RequireTribute"] = true | |||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes | |||
title := ctx.Query("title") | |||
ctx.Data["TitleQuery"] = title | |||
body := ctx.Query("body") | |||
ctx.Data["BodyQuery"] = body | |||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects) | |||
@@ -562,10 +599,10 @@ func NewIssue(ctx *context.Context) { | |||
} | |||
setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates) | |||
renderAttachmentSettings(ctx) | |||
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false) | |||
setTemplateIfExists(ctx, issueTemplateKey, context.IssueTemplateDirCandidates, IssueTemplateCandidates) | |||
if ctx.Written() { | |||
return | |||
} | |||
@@ -575,6 +612,19 @@ func NewIssue(ctx *context.Context) { | |||
ctx.HTML(200, tplIssueNew) | |||
} | |||
// NewIssueChooseTemplate render creating issue from template page | |||
func NewIssueChooseTemplate(ctx *context.Context) { | |||
ctx.Data["Title"] = ctx.Tr("repo.issues.new") | |||
ctx.Data["PageIsIssueList"] = true | |||
ctx.Data["milestone"] = ctx.QueryInt64("milestone") | |||
issueTemplates := ctx.IssueTemplatesFromDefaultBranch() | |||
ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0 | |||
ctx.Data["IssueTemplates"] = issueTemplates | |||
ctx.HTML(200, tplIssueChoose) | |||
} | |||
// ValidateRepoMetas check and returns repository's meta informations | |||
func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) { | |||
var ( | |||
@@ -676,6 +726,7 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull b | |||
func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { | |||
ctx.Data["Title"] = ctx.Tr("repo.issues.new") | |||
ctx.Data["PageIsIssueList"] = true | |||
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 | |||
ctx.Data["RequireHighlightJS"] = true | |||
ctx.Data["RequireSimpleMDE"] = true | |||
ctx.Data["ReadOnly"] = false | |||
@@ -814,6 +865,7 @@ func ViewIssue(ctx *context.Context) { | |||
return | |||
} | |||
ctx.Data["PageIsIssueList"] = true | |||
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 | |||
} | |||
if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) { | |||
@@ -264,6 +264,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { | |||
ctx.Data["Milestone"] = milestone | |||
issues(ctx, milestoneID, 0, util.OptionalBoolNone) | |||
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 | |||
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false) | |||
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true) | |||
@@ -723,8 +723,11 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
// Grouping for those endpoints that do require authentication | |||
m.Group("/:username/:reponame", func() { | |||
m.Group("/issues", func() { | |||
m.Combo("/new").Get(context.RepoRef(), repo.NewIssue). | |||
Post(bindIgnErr(auth.CreateIssueForm{}), repo.NewIssuePost) | |||
m.Group("/new", func() { | |||
m.Combo("").Get(context.RepoRef(), repo.NewIssue). | |||
Post(bindIgnErr(auth.CreateIssueForm{}), repo.NewIssuePost) | |||
m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) | |||
}) | |||
}, context.RepoMustNotBeArchived(), reqRepoIssueReader) | |||
// FIXME: should use different URLs but mostly same logic for comments of issue and pull reuqest. | |||
// So they can apply their own enable/disable logic on routers. | |||
@@ -0,0 +1,25 @@ | |||
{{template "base/head" .}} | |||
<div class="repository new issue"> | |||
{{template "repo/header" .}} | |||
<div class="ui container"> | |||
<div class="navbar"> | |||
{{template "repo/issue/navbar" .}} | |||
</div> | |||
<div class="ui divider"></div> | |||
{{range .IssueTemplates}} | |||
<div class="ui attached segment"> | |||
<div class="ui two column grid"> | |||
<div class="column left aligned"> | |||
<strong>{{.Name | RenderEmojiPlain}}</strong> | |||
<br/>{{.About | RenderEmojiPlain}} | |||
</div> | |||
<div class="column right aligned"> | |||
<a href="{{$.RepoLink}}/issues/new?template={{.FileName}}{{if $.milestone}}&milestone={{$.milestone}}{{end}}" class="ui green button">{{$.i18n.Tr "repo.issues.choose.get_started"}}</a> | |||
</div> | |||
</div> | |||
</div> | |||
{{end}} | |||
<a href="{{.RepoLink}}/issues/new{{if .milestone}}?milestone={{.milestone}}{{end}}">{{.i18n.Tr "repo.issues.choose.blank"}}</a> | |||
</div> | |||
</div> | |||
{{template "base/footer" .}} |
@@ -12,7 +12,7 @@ | |||
{{if not .Repository.IsArchived}} | |||
<div class="column right aligned"> | |||
{{if .PageIsIssueList}} | |||
<a class="ui green button" href="{{.RepoLink}}/issues/new">{{.i18n.Tr "repo.issues.new"}}</a> | |||
<a class="ui green button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{.i18n.Tr "repo.issues.new"}}</a> | |||
{{else}} | |||
<a class="ui green button {{if not .PullRequestCtx.Allowed}}disabled{{end}}" href="{{if .PullRequestCtx.Allowed}}{{.Repository.Link}}/compare/{{.Repository.DefaultBranch | EscapePound}}...{{if ne .Repository.Owner.Name .PullRequestCtx.BaseRepo.Owner.Name}}{{.Repository.Owner.Name}}:{{end}}{{.Repository.DefaultBranch | EscapePound}}{{end}}">{{.i18n.Tr "repo.pulls.new"}}</a> | |||
{{end}} | |||
@@ -16,7 +16,7 @@ | |||
{{if or .CanWriteIssues .CanWritePulls}} | |||
<a class="ui grey button" href="{{.RepoLink}}/milestones/{{.MilestoneID}}/edit">{{.i18n.Tr "repo.milestones.edit"}}</a> | |||
{{end}} | |||
<a class="ui green button" href="{{.RepoLink}}/issues/new?milestone={{.MilestoneID}}">{{.i18n.Tr "repo.issues.new"}}</a> | |||
<a class="ui green button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}?milestone={{.MilestoneID}}">{{.i18n.Tr "repo.issues.new"}}</a> | |||
</div> | |||
{{end}} | |||
</div> | |||
@@ -13,7 +13,7 @@ | |||
</a> | |||
<div class="ui segment content"> | |||
<div class="field"> | |||
<input name="title" id="issue_title" placeholder="{{.i18n.Tr "repo.milestones.title"}}" value="{{.title}}" tabindex="3" autofocus required maxlength="255"> | |||
<input name="title" id="issue_title" placeholder="{{.i18n.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" tabindex="3" autofocus required maxlength="255"> | |||
{{if .PageIsComparePull}} | |||
<div class="title_wip_desc" data-wip-prefixes="{{Json .PullRequestWorkInProgressPrefixes}}">{{.i18n.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div> | |||
{{end}} | |||
@@ -9,7 +9,7 @@ | |||
{{if not .Repository.IsArchived}} | |||
<div class="column right aligned"> | |||
{{if .PageIsIssueList}} | |||
<a class="ui green button" href="{{.RepoLink}}/issues/new">{{.i18n.Tr "repo.issues.new"}}</a> | |||
<a class="ui green button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{.i18n.Tr "repo.issues.new"}}</a> | |||
{{else}} | |||
<a class="ui green button {{if not .PullRequestCtx.Allowed}}disabled{{end}}" href="{{.RepoLink}}/compare/{{.BranchName | EscapePound}}...{{.PullRequestCtx.HeadInfo | EscapePound}}">{{.i18n.Tr "repo.pulls.new"}}</a> | |||
{{end}} | |||
@@ -3852,6 +3852,39 @@ | |||
} | |||
} | |||
}, | |||
"/repos/{owner}/{repo}/issue_templates": { | |||
"get": { | |||
"produces": [ | |||
"application/json" | |||
], | |||
"tags": [ | |||
"repository" | |||
], | |||
"summary": "Get available issue templates for a repository", | |||
"operationId": "repoGetIssueTemplates", | |||
"parameters": [ | |||
{ | |||
"type": "string", | |||
"description": "owner of the repo", | |||
"name": "owner", | |||
"in": "path", | |||
"required": true | |||
}, | |||
{ | |||
"type": "string", | |||
"description": "name of the repo", | |||
"name": "repo", | |||
"in": "path", | |||
"required": true | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"$ref": "#/responses/IssueTemplates" | |||
} | |||
} | |||
} | |||
}, | |||
"/repos/{owner}/{repo}/issues": { | |||
"get": { | |||
"produces": [ | |||
@@ -13439,6 +13472,40 @@ | |||
}, | |||
"x-go-package": "code.gitea.io/gitea/modules/structs" | |||
}, | |||
"IssueTemplate": { | |||
"description": "IssueTemplate represents an issue template for a repository", | |||
"type": "object", | |||
"properties": { | |||
"about": { | |||
"type": "string", | |||
"x-go-name": "About" | |||
}, | |||
"content": { | |||
"type": "string", | |||
"x-go-name": "Content" | |||
}, | |||
"file_name": { | |||
"type": "string", | |||
"x-go-name": "FileName" | |||
}, | |||
"labels": { | |||
"type": "array", | |||
"items": { | |||
"type": "string" | |||
}, | |||
"x-go-name": "Labels" | |||
}, | |||
"name": { | |||
"type": "string", | |||
"x-go-name": "Name" | |||
}, | |||
"title": { | |||
"type": "string", | |||
"x-go-name": "Title" | |||
} | |||
}, | |||
"x-go-package": "code.gitea.io/gitea/modules/structs" | |||
}, | |||
"Label": { | |||
"description": "Label a label to an issue or a pr", | |||
"type": "object", | |||
@@ -15480,6 +15547,15 @@ | |||
} | |||
} | |||
}, | |||
"IssueTemplates": { | |||
"description": "IssueTemplates", | |||
"schema": { | |||
"type": "array", | |||
"items": { | |||
"$ref": "#/definitions/IssueTemplate" | |||
} | |||
} | |||
}, | |||
"Label": { | |||
"description": "Label", | |||
"schema": { | |||