Skip to content

WIP: Implement Issue forms #20778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 81 additions & 6 deletions modules/context/repo.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
"path"
"strconv"
"strings"

"code.gitea.io/gitea/models"
@@ -35,6 +36,7 @@ import (
asymkey_service "code.gitea.io/gitea/services/asymkey"

"github.com/editorconfig/editorconfig-core-go/v2"
"gopkg.in/yaml.v2"
)

// IssueTemplateDirCandidates issue templates directory
@@ -1032,19 +1034,52 @@ func UnitTypes() func(ctx *Context) {
}
}

func ExtractTemplateFromYaml(templateContent []byte, meta *api.IssueTemplate) (*api.IssueFormTemplate, []string, error) {
var tmpl *api.IssueFormTemplate
err := yaml.Unmarshal(templateContent, &tmpl)
if err != nil {
return nil, nil, err
}

// Make sure it's valid
if validationErrs := tmpl.Valid(); len(validationErrs) > 0 {
return nil, validationErrs, fmt.Errorf("invalid issue template: %v", validationErrs)
}

// Fill missing field IDs with the field index
for i, f := range tmpl.Fields {
if f.ID == "" {
tmpl.Fields[i].ID = strconv.FormatInt(int64(i+1), 10)
}
}

// Copy metadata
if meta != nil {
meta.Name = tmpl.Name
meta.Title = tmpl.Title
meta.About = tmpl.About
meta.Labels = tmpl.Labels
// TODO: meta.Assignees = tmpl.Assignees
meta.Ref = tmpl.Ref
}

return tmpl, nil, nil
}

// IssueTemplatesFromDefaultBranch checks for issue templates in the repo's default branch
func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate {
func (ctx *Context) IssueTemplatesFromDefaultBranch() ([]api.IssueTemplate, map[string][]string) {
var issueTemplates []api.IssueTemplate
validationErrs := make(map[string][]string)

if ctx.Repo.Repository.IsEmpty {
return issueTemplates
return issueTemplates, nil
}

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
return issueTemplates, nil
}
}

@@ -1055,7 +1090,7 @@ func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate {
}
entries, err := tree.ListEntries()
if err != nil {
return issueTemplates
return issueTemplates, nil
}
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".md") {
@@ -1088,14 +1123,54 @@ func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate {
}
it.Content = content
it.FileName = entry.Name()
if it.Valid() {
issueTemplates = append(issueTemplates, it)
} else {
fmt.Printf("%#v\n", it)
}
} else if strings.HasSuffix(entry.Name(), ".yaml") || strings.HasSuffix(entry.Name(), ".yml") {
if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
log.Debug("Issue form template is too large: %s", entry.Name())
continue
}
r, err := entry.Blob().DataAsync()
if err != nil {
log.Debug("DataAsync: %v", err)
continue
}
closed := false
defer func() {
if !closed {
_ = r.Close()
}
}()
templateContent, err := io.ReadAll(r)
if err != nil {
log.Debug("ReadAll: %v", err)
continue
}
_ = r.Close()

var it api.IssueTemplate
it.FileName = path.Base(entry.Name())

var tmplValidationErrs []string
_, tmplValidationErrs, err = ExtractTemplateFromYaml(templateContent, &it)
if err != nil {
log.Debug("ExtractTemplateFromYaml: %v", err)
if tmplValidationErrs != nil {
validationErrs[path.Base(entry.Name())] = tmplValidationErrs
}
continue
}
if it.Valid() {
issueTemplates = append(issueTemplates, it)
}
}
}
if len(issueTemplates) > 0 {
return issueTemplates
return issueTemplates, validationErrs
}
}
return issueTemplates
return issueTemplates, validationErrs
}
100 changes: 100 additions & 0 deletions modules/structs/issue_form.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2016 The Gogs 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 structs

import (
"fmt"
"strings"
)

type FormField struct {
Type string `yaml:"type"`
ID string `yaml:"id"`
Attributes map[string]interface{} `yaml:"attributes"`
Validations map[string]interface{} `yaml:"validations"`
}

// IssueFormTemplate represents an issue form template for a repository
// swagger:model
type IssueFormTemplate struct {
Name string `yaml:"name"`
Title string `yaml:"title"`
About string `yaml:"description"`
Labels []string `yaml:"labels"`
Assignees []string `yaml:"assignees"`
Ref string `yaml:"ref"`
Fields []FormField `yaml:"body"`
FileName string `yaml:"-"`
}

// Valid checks whether an IssueFormTemplate is considered valid, e.g. at least name and about, and labels for all fields
func (it IssueFormTemplate) Valid() []string {
// TODO: Localize error messages
// TODO: Add a bunch more validations
var errs []string

if strings.TrimSpace(it.Name) == "" {
errs = append(errs, "the 'name' field of the issue template are required")
}
if strings.TrimSpace(it.About) == "" {
errs = append(errs, "the 'about' field of the issue template are required")
}

// Make sure all non-markdown fields have labels
for fieldIdx, field := range it.Fields {
// Make checker functions
checkStringAttr := func(attrName string) {
attr := field.Attributes[attrName]
if attr == nil || strings.TrimSpace(attr.(string)) == "" {
errs = append(errs, fmt.Sprintf(
"(field #%d '%s'): the '%s' attribute is required for fields with type %s",
fieldIdx+1, field.ID, attrName, field.Type,
))
}
}
checkOptionsStringAttr := func(optionIdx int, option map[interface{}]interface{}, attrName string) {
attr := option[attrName]
if attr == nil || strings.TrimSpace(attr.(string)) == "" {
errs = append(errs, fmt.Sprintf(
"(field #%d '%s', option #%d): the '%s' field is required for options",
fieldIdx+1, field.ID, optionIdx, attrName,
))
}
}
checkListAttr := func(attrName string, itemChecker func(int, map[interface{}]interface{})) {
attr := field.Attributes[attrName]
if attr == nil {
errs = append(errs, fmt.Sprintf(
"(field #%d '%s'): the '%s' attribute is required for fields with type %s",
fieldIdx+1, field.ID, attrName, field.Type,
))
} else {
for i, item := range attr.([]interface{}) {
itemChecker(i, item.(map[interface{}]interface{}))
}
}
}

// Make sure each field has its attributes
switch field.Type {
case "markdown":
checkStringAttr("value")
case "textarea", "input", "dropdown":
checkStringAttr("label")
case "checkboxes":
checkStringAttr("label")
checkListAttr("options", func(i int, item map[interface{}]interface{}) {
checkOptionsStringAttr(i, item, "label")
})
default:
errs = append(errs, fmt.Sprintf(
"(field #%d '%s'): unknown type '%s'",
fieldIdx+1, field.ID, field.Type,
))
}
}

return errs
}
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
@@ -75,6 +75,7 @@ forks = Forks
activities = Activities
pull_requests = Pull Requests
issues = Issues
issue = Issue
milestones = Milestones

ok = OK
@@ -1202,6 +1203,7 @@ issues.filter_labels = Filter Label
issues.filter_reviewers = Filter Reviewer
issues.new = New Issue
issues.new.title_empty = Title cannot be empty
issues.new.invalid_form_values = Invalid form values
issues.new.labels = Labels
issues.new.add_labels_title = Apply labels
issues.new.no_label = No Label
3 changes: 2 additions & 1 deletion routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
@@ -1080,5 +1080,6 @@ func GetIssueTemplates(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/IssueTemplates"

ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch())
issueTemplates, _ := ctx.IssueTemplatesFromDefaultBranch()
ctx.JSON(http.StatusOK, issueTemplates)
}
221 changes: 178 additions & 43 deletions routers/web/repo/issue.go
Original file line number Diff line number Diff line change
@@ -64,6 +64,8 @@ const (
tplReactions base.TplName = "repo/issue/view_content/reactions"

issueTemplateKey = "IssueTemplate"
issueFormTemplateKey = "IssueFormTemplate"
issueFormErrorsKey = "IssueTemplateErrors"
issueTemplateTitleKey = "IssueTemplateTitle"
)

@@ -407,7 +409,8 @@ func Issues(ctx *context.Context) {
}
ctx.Data["Title"] = ctx.Tr("repo.issues")
ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
issueTemplates, _ := ctx.IssueTemplatesFromDefaultBranch()
ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0
}

issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
@@ -722,16 +725,16 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
return labels
}

func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
if ctx.Repo.Commit == nil {
func getFileContentFromDefaultBranch(repo *context.Repository, filename string) (string, bool) {
if repo.Commit == nil {
var err error
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
repo.Commit, err = repo.GitRepo.GetBranchCommit(repo.Repository.DefaultBranch)
if err != nil {
return "", false
}
}

entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
entry, err := repo.Commit.GetTreeEntryByPath(filename)
if err != nil {
return "", false
}
@@ -750,60 +753,101 @@ func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (str
return string(bytes), true
}

func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) {
func getTemplate(repo *context.Repository, template string, possibleDirs, possibleFiles []string) (*api.IssueTemplate, string, *api.IssueFormTemplate, map[string][]string, error) {
validationErrs := make(map[string][]string)

// Add `possibleFiles` and each `{possibleDirs}/{template}` to `templateCandidates`
templateCandidates := make([]string, 0, len(possibleFiles))
if ctx.FormString("template") != "" {
if template != "" {
for _, dirName := range possibleDirs {
templateCandidates = append(templateCandidates, path.Join(dirName, ctx.FormString("template")))
templateCandidates = append(templateCandidates, path.Join(dirName, template))
}
}
templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback

for _, filename := range templateCandidates {
templateContent, found := getFileContentFromDefaultBranch(ctx, filename)
// Read each template
templateContent, found := getFileContentFromDefaultBranch(repo, filename)
if found {
var meta api.IssueTemplate
templateBody, err := markdown.ExtractMetadata(templateContent, &meta)
meta := api.IssueTemplate{FileName: filename}
var templateBody string
var formTemplateBody *api.IssueFormTemplate
var err error

if strings.HasSuffix(filename, ".md") {
// Parse markdown template
templateBody, err = markdown.ExtractMetadata(templateContent, &meta)
} else if strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml") {
// Parse yaml (form) template
var tmplValidationErrs []string
formTemplateBody, tmplValidationErrs, err = context.ExtractTemplateFromYaml([]byte(templateContent), &meta)
if err == nil {
formTemplateBody.FileName = path.Base(filename)
} else if tmplValidationErrs != nil {
validationErrs[path.Base(filename)] = tmplValidationErrs
}
} else {
err = errors.New("invalid template type")
}
if err != nil {
log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
ctx.Data[ctxDataKey] = templateContent
return
log.Debug("could not extract metadata from %s [%s]: %v", filename, repo.Repository.FullName(), err)
}
ctx.Data[issueTemplateTitleKey] = meta.Title
ctx.Data[ctxDataKey] = templateBody
labelIDs := make([]string, 0, len(meta.Labels))
if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
ctx.Data["Labels"] = repoLabels
if ctx.Repo.Owner.IsOrganization() {
if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
ctx.Data["OrgLabels"] = orgLabels
repoLabels = append(repoLabels, orgLabels...)
}
}

for _, metaLabel := range meta.Labels {
for _, repoLabel := range repoLabels {
if strings.EqualFold(repoLabel.Name, metaLabel) {
repoLabel.IsChecked = true
labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
break
}
}
return &meta, templateBody, formTemplateBody, validationErrs, err
}
}

return nil, "", nil, validationErrs, errors.New("no template found")
}

func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) {
templateMeta, templateBody, formTemplateBody, validationErrs, err := getTemplate(ctx.Repo, ctx.FormString("template"), possibleDirs, possibleFiles)
if err != nil {
return
}

if formTemplateBody != nil {
ctx.Data[issueFormTemplateKey] = formTemplateBody
}
if len(validationErrs) > 0 {
ctx.Data[issueFormErrorsKey] = validationErrs
}

ctx.Data[issueTemplateTitleKey] = templateMeta.Title
ctx.Data[ctxDataKey] = templateBody

labelIDs := make([]string, 0, len(templateMeta.Labels))
if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
ctx.Data["Labels"] = repoLabels
if ctx.Repo.Owner.IsOrganization() {
if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
ctx.Data["OrgLabels"] = orgLabels
repoLabels = append(repoLabels, orgLabels...)
}
}

for _, metaLabel := range templateMeta.Labels {
for _, repoLabel := range repoLabels {
if strings.EqualFold(repoLabel.Name, metaLabel) {
repoLabel.IsChecked = true
labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
break
}
}
ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
ctx.Data["Reference"] = meta.Ref
ctx.Data["RefEndName"] = git.RefEndName(meta.Ref)
return
}
}
ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
ctx.Data["Reference"] = templateMeta.Ref
ctx.Data["RefEndName"] = git.RefEndName(templateMeta.Ref)
}

// NewIssue render creating issue page
func NewIssue(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
issueTemplates, _ := ctx.IssueTemplatesFromDefaultBranch()
ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0
ctx.Data["RequireTribute"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
title := ctx.FormString("title")
@@ -860,7 +904,10 @@ func NewIssueChooseTemplate(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true

issueTemplates := ctx.IssueTemplatesFromDefaultBranch()
issueTemplates, validationErrs := ctx.IssueTemplatesFromDefaultBranch()
if len(validationErrs) > 0 {
ctx.Data[issueFormErrorsKey] = validationErrs
}
ctx.Data["IssueTemplates"] = issueTemplates

if len(issueTemplates) == 0 {
@@ -997,12 +1044,86 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
return labelIDs, assigneeIDs, milestoneID, form.ProjectID
}

// Renders the given form values to Markdown
// Returns an empty string if user submitted a non-form issue
func renderIssueFormValues(ctx *context.Context, form *url.Values) (string, error) {
// Skip if submitted without a form
if form.Has("content") || !form.Has("form-type") {
return "", nil
}

// Fetch template
_, _, formTemplateBody, _, err := getTemplate(
ctx.Repo,
form.Get("form-type"),
context.IssueTemplateDirCandidates,
IssueTemplateCandidates,
)
if err != nil {
return "", err
}
if formTemplateBody == nil {
return "", errors.New("no form template found")
}

// Render values
result := ""
for _, field := range formTemplateBody.Fields {
if field.ID != "" {
// Get field label
label := field.Attributes["label"]
if label == "" {
label = field.ID
}

// Format the value into Markdown
if field.Type == "markdown" {
// Markdown blocks do not appear in output
} else if field.Type == "checkboxes" || (field.Type == "dropdown" && field.Attributes["multiple"] == true) {
result += fmt.Sprintf("### %s\n", label)
for i, option := range field.Attributes["options"].([]interface{}) {
// Get "checked" value
checkedStr := " "
isChecked := form.Get(fmt.Sprintf("form-field-%s-%d", field.ID, i)) == "on"
if isChecked {
checkedStr = "x"
} else if field.Type == "checkboxes" && (option.(map[interface{}]interface{})["required"] == true && !isChecked) {
return "", fmt.Errorf("checkbox #%d in field '%s' is required, but not checked", i, field.ID)
}

// Get label
var label string
if field.Type == "checkboxes" {
label = option.(map[interface{}]interface{})["label"].(string)
} else {
label = option.(string)
}
result += fmt.Sprintf("- [%s] %s\n", checkedStr, label)
}
result += "\n"
} else if field.Type == "input" || field.Type == "textarea" || field.Type == "dropdown" {
if renderType, ok := field.Attributes["render"]; ok {
result += fmt.Sprintf("### %s\n```%s\n%s\n```\n\n", label, renderType, form.Get("form-field-"+field.ID))
} else {
result += fmt.Sprintf("### %s\n%s\n\n", label, form.Get("form-field-"+field.ID))
}
} else {
// Template should have been validated at this point
panic(fmt.Errorf("Invalid field type: '%s'", field.Type))
}
}
}

return result, nil
}

// NewIssuePost response for creating new issue
func NewIssuePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateIssueForm)
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
issueTemplates, _ := ctx.IssueTemplatesFromDefaultBranch()
ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
@@ -1031,14 +1152,27 @@ func NewIssuePost(ctx *context.Context) {
return
}

// If the issue submitted is a form, render it to Markdown
issueContents, err := renderIssueFormValues(ctx, &ctx.Req.Form)
if err != nil {
ctx.Flash.ErrorMsg = ctx.Tr("repo.issues.new.invalid_form_values")
ctx.Data["Flash"] = ctx.Flash
NewIssue(ctx)
return
}
if issueContents == "" {
// Not a form
issueContents = form.Content
}

issue := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
Title: form.Title,
PosterID: ctx.Doer.ID,
Poster: ctx.Doer,
MilestoneID: milestoneID,
Content: form.Content,
Content: issueContents,
Ref: form.Ref,
}

@@ -1185,7 +1319,8 @@ func ViewIssue(ctx *context.Context) {
return
}
ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
issueTemplates, _ := ctx.IssueTemplatesFromDefaultBranch()
ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0
}

if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
3 changes: 2 additions & 1 deletion routers/web/repo/milestone.go
Original file line number Diff line number Diff line change
@@ -290,7 +290,8 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
ctx.Data["Milestone"] = milestone

issues(ctx, milestoneID, 0, util.OptionalBoolNone)
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
issueTemplates, _ := ctx.IssueTemplatesFromDefaultBranch()
ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0

ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
14 changes: 14 additions & 0 deletions templates/repo/issue/choose.tmpl
Original file line number Diff line number Diff line change
@@ -30,6 +30,20 @@
</div>
</div>
</div>
{{- if .IssueTemplateErrors}}
<div class="ui warning message">
<div class="text left">
<div>The following issue templates have errors:</div>
<ul>
{{range $filename, $errors := .IssueTemplateErrors}}
{{range $errors}}
<li>{{$filename}}: {{.}}</li>
{{end}}
{{end}}
</ul>
</div>
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}
95 changes: 80 additions & 15 deletions templates/repo/issue/comment_tab.tmpl
Original file line number Diff line number Diff line change
@@ -1,19 +1,84 @@
<div class="ui top tabular menu" data-write="write" data-preview="preview">
<a class="active item" data-tab="write">{{.locale.Tr "write"}}</a>
<a class="item" data-tab="preview" data-url="{{.Repository.HTMLURL}}/markdown" data-context="{{.RepoLink}}">{{.locale.Tr "preview"}}</a>
</div>
<div class="field">
<div class="ui bottom active tab" data-tab="write">
<textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{.Repository.HTMLURL}}/markdown" data-context="{{.Repo.RepoLink}}">
{{- if .BodyQuery}}{{.BodyQuery}}{{else if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end -}}
</textarea>
{{- if .IssueFormTemplate}}
<input type="hidden" name="form-type" value="{{.IssueFormTemplate.FileName}}">
{{range $field_idx, $field := .IssueFormTemplate.Fields}}
{{- if $field.Attributes.label}}<h3 id="form-label-{{$field_idx}}">{{$field.Attributes.label}} {{if $field.Validations.required}} <span style="color: orangered;">*</span>{{end}}</h3>{{end}}
{{- if $field.Attributes.description}}<div>{{RenderMarkdownToHtml $field.Attributes.description}}</div>{{end}}

{{- if eq .Type "markdown"}}
<div>{{RenderMarkdownToHtml $field.Attributes.value}}</div>
{{else if eq .Type "input"}}
<input type="text"
aria-labelledby="form-label-{{$field_idx}}"
name="form-field-{{$field.ID}}"
id="form-field-{{$field.ID}}"
placeholder="{{$field.Attributes.placeholder}}"
tabindex="3"
value="{{$field.Attributes.value}}"
{{- if .Validations.required}}required{{end}} />
{{else if eq .Type "textarea"}}
{{- if .Attributes.render}}
<input type="hidden" name="attrs-form-field-{{$field.ID}}" value="{&quot;render&quot;: &quot;{{.Attributes.render}}&quot;}">
{{end}}
<textarea aria-labelledby="form-label-{{$field_idx}}"
name="form-field-{{$field.ID}}"
id="form-field-{{$field.ID}}"
placeholder="{{$field.Attributes.placeholder}}"
tabindex="3"
{{- if .Attributes.render}}class="no-easymde"{{end}}
{{- if .Validations.required}}required{{end}}>{{$field.Attributes.value}}</textarea>
{{else if eq .Type "checkboxes"}}
{{range $chk_id, $chk := $field.Attributes.options}}
<label>
<input type="checkbox"
name="form-field-{{$field.ID}}-{{$chk_id}}"
id="form-field-{{$field.ID}}-{{$chk_id}}"
tabindex="3"
{{if $chk.checked}}checked{{end}}
{{if $chk.required}}fixme-required{{end}} />
{{$chk.label}}{{if $chk.required}} <span style="color: orangered;">*</span>{{end}}
</label><br>
{{end}}
{{else if and (eq .Type "dropdown") $field.Attributes.multiple}}
{{range $chk_id, $chk := $field.Attributes.options}}
<label>
<input type="checkbox"
name="form-field-{{$field.ID}}-{{$chk_id}}"
tabindex="3"
id="form-field-{{$field.ID}}-{{$chk_id}}" />
{{$chk}}
</label><br>
{{end}}
{{else if eq .Type "dropdown"}}
<select aria-labelledby="form-label-{{$field_idx}}"
name="form-field-{{$field.ID}}"
id="form-field-{{$field.ID}}"
tabindex="3"
{{- if .Validations.required}}required{{end}}>
{{range $field.Attributes.options}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
{{end}}

{{end}}
{{else}}
<div class="ui top tabular menu" data-write="write" data-preview="preview">
<a class="active item" data-tab="write">{{.locale.Tr "write"}}</a>
<a class="item" data-tab="preview" data-url="{{.Repository.HTMLURL}}/markdown" data-context="{{.RepoLink}}">{{.locale.Tr "preview"}}</a>
</div>
<div class="ui bottom tab markup" data-tab="preview">
{{.locale.Tr "loading"}}
</div>
</div>
{{if .IsAttachmentEnabled}}
<div class="field">
{{template "repo/upload" .}}
<div class="ui bottom active tab" data-tab="write">
<textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{.Repository.HTMLURL}}/markdown" data-context="{{.Repo.RepoLink}}">
{{- if .BodyQuery}}{{.BodyQuery}}{{else if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end -}}
</textarea>
</div>
<div class="ui bottom tab markup" data-tab="preview">
{{.locale.Tr "loading"}}
</div>
</div>
{{if .IsAttachmentEnabled}}
<div class="field">
{{template "repo/upload" .}}
</div>
{{end}}
{{end}}
8 changes: 7 additions & 1 deletion templates/repo/issue/new_form.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<form class="ui comment form stackable grid" id="new-issue" action="{{.Link}}" method="post">
{{- if .IssueFormTemplate}}
<h1>{{.locale.Tr "issue"}}: {{.IssueFormTemplate.Name}}</h1>
<span>{{.IssueFormTemplate.About}}</span>
<br>
<br>
{{end}}
<form class="ui comment form stackable grid" id="new-issue" action="{{.Data.CurrentURL}}" method="post">
{{.CsrfTokenHtml}}
{{if .Flash}}
<div class="sixteen wide column">
2 changes: 1 addition & 1 deletion web_src/js/features/comp/EasyMDE.js
Original file line number Diff line number Diff line change
@@ -93,7 +93,7 @@ export async function createCommentEasyMDE(textarea, easyMDEOptions = {}) {
cm.execCommand('delCharBefore');
},
});
attachTribute(inputField, {mentions: true, emoji: true});
await attachTribute(inputField, {mentions: true, emoji: true});
attachEasyMDEToElements(easyMDE);
return easyMDE;
}
11 changes: 8 additions & 3 deletions web_src/js/features/repo-legacy.js
Original file line number Diff line number Diff line change
@@ -68,9 +68,14 @@ export function initRepoCommentForm() {
}

(async () => {
const $textarea = $commentForm.find('textarea:not(.review-textarea)');
const easyMDE = await createCommentEasyMDE($textarea);
initEasyMDEImagePaste(easyMDE, $commentForm.find('.dropzone'));
for (const textarea of $commentForm.find('textarea:not(.review-textarea, .no-easymde)')) {
// Don't initialize EasyMDE for the dormant #edit-content-form
if (textarea.closest('#edit-content-form')) {
continue;
}
const easyMDE = await createCommentEasyMDE(textarea);
initEasyMDEImagePaste(easyMDE, $commentForm.find('.dropzone'));
}
})();

initBranchSelector();