Issue templates directory (#11450)

* 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>
This commit is contained in:
John Olheiser 2020-09-11 09:48:39 -05:00 committed by GitHub
parent dd1a651b58
commit 26c4a049da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 381 additions and 17 deletions

View file

@ -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())
})

View file

@ -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())
}

View file

@ -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 {

View file

@ -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)

View file

@ -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) {

View file

@ -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)

View file

@ -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.