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:
		
							parent
							
								
									dd1a651b58
								
							
						
					
					
						commit
						26c4a049da
					
				
					 18 changed files with 381 additions and 17 deletions
				
			
		|  | @ -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 | ||||
| } | ||||
|  |  | |||
							
								
								
									
										49
									
								
								modules/markup/markdown/meta.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								modules/markup/markdown/meta.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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.
 | ||||
|  |  | |||
							
								
								
									
										25
									
								
								templates/repo/issue/choose.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								templates/repo/issue/choose.tmpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -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": { | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue