From d9cacea681c709f843a037c3c6fd7e3bd619deb5 Mon Sep 17 00:00:00 2001 From: David Shlemayev Date: Sat, 13 Aug 2022 03:30:54 +0300 Subject: [PATCH 1/8] Support multiple EasyMDE editors in a single page --- web_src/js/features/comp/EasyMDE.js | 2 +- web_src/js/features/repo-legacy.js | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/web_src/js/features/comp/EasyMDE.js b/web_src/js/features/comp/EasyMDE.js index f1b4b0efc398d..015fc3505082b 100644 --- a/web_src/js/features/comp/EasyMDE.js +++ b/web_src/js/features/comp/EasyMDE.js @@ -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; } diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 11c97ccfb011c..9d8796b5a6d55 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -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(); From a45f362d5ec94bd59da58fc39b5d336d40800894 Mon Sep 17 00:00:00 2001 From: David Shlemayev Date: Sat, 13 Aug 2022 03:59:49 +0300 Subject: [PATCH 2/8] Add initial support for issue form templates --- modules/context/repo.go | 53 ++++++++ modules/structs/issue_form.go | 43 ++++++ options/locale/locale_en-US.ini | 1 + routers/web/repo/issue.go | 182 ++++++++++++++++++++------ templates/repo/issue/comment_tab.tmpl | 82 +++++++++--- templates/repo/issue/new_form.tmpl | 6 + 6 files changed, 314 insertions(+), 53 deletions(-) create mode 100644 modules/structs/issue_form.go diff --git a/modules/context/repo.go b/modules/context/repo.go index ea40542069991..b7768efe14f7c 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -8,6 +8,7 @@ package context import ( "context" "fmt" + "gopkg.in/yaml.v2" "html" "io" "net/http" @@ -1032,6 +1033,25 @@ func UnitTypes() func(ctx *Context) { } } +func ExtractTemplateFromYaml(templateContent []byte, meta *api.IssueTemplate) (tmpl *api.IssueFormTemplate, err error) { + err = yaml.Unmarshal(templateContent, &tmpl) + if err != nil { + return nil, err + } + + // 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 +} + // IssueTemplatesFromDefaultBranch checks for issue templates in the repo's default branch func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate { var issueTemplates []api.IssueTemplate @@ -1091,6 +1111,39 @@ func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate { if it.Valid() { issueTemplates = append(issueTemplates, 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()) + _, err = ExtractTemplateFromYaml(templateContent, &it) + if err != nil { + log.Debug("ExtractTemplateFromYaml: %v", err) + continue + } + if it.Valid() { + issueTemplates = append(issueTemplates, it) + } } } if len(issueTemplates) > 0 { diff --git a/modules/structs/issue_form.go b/modules/structs/issue_form.go new file mode 100644 index 0000000000000..303062ead5385 --- /dev/null +++ b/modules/structs/issue_form.go @@ -0,0 +1,43 @@ +// 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 "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 +func (it IssueFormTemplate) Valid() bool { + if strings.TrimSpace(it.Name) == "" || strings.TrimSpace(it.About) == "" { + return false + } + + for _, field := range it.Fields { + if strings.TrimSpace(field.Id) == "" { + // TODO: add IDs should be optional, maybe generate slug from label? or use numberic id + return false + } + } + + return true +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 2330057f87202..39874122098d9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -75,6 +75,7 @@ forks = Forks activities = Activities pull_requests = Pull Requests issues = Issues +issue = Issue milestones = Milestones ok = OK diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index ad25a94e13b19..fe48c14b6b8d2 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -64,6 +64,7 @@ const ( tplReactions base.TplName = "repo/issue/view_content/reactions" issueTemplateKey = "IssueTemplate" + issueFormTemplateKey = "IssueFormTemplate" issueTemplateTitleKey = "IssueTemplateTitle" ) @@ -722,16 +723,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,53 +751,89 @@ 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) ( + outMeta *api.IssueTemplate, + outTemplateBody string, + outFormTemplateBody *api.IssueFormTemplate, + err error, +) { + // 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} + + if strings.HasSuffix(filename, ".md") { + // Parse markdown template + outTemplateBody, err = markdown.ExtractMetadata(templateContent, meta) + } else if strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml") { + // Parse yaml (form) template + outFormTemplateBody, err = context.ExtractTemplateFromYaml([]byte(templateContent), &meta) + outFormTemplateBody.FileName = path.Base(filename) + } 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) + outTemplateBody = templateContent + err = nil } - 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 - } - } + outMeta = &meta + return + } + } + err = errors.New("no template found") + return +} + +func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) { + templateMeta, templateBody, formTemplateBody, err := getTemplate(ctx.Repo, ctx.FormString("template"), possibleDirs, possibleFiles) + if err != nil { + return + } + + if formTemplateBody != nil { + ctx.Data[issueFormTemplateKey] = formTemplateBody + } + + 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) + return } // NewIssue render creating issue page @@ -997,6 +1034,65 @@ 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 + switch field.Type { + case "markdown": + // Markdown blocks do not appear in output + case "input", "textarea", "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)) + } + case "checkboxes": + result += fmt.Sprintf("### %s\n", label) + for i, option := range field.Attributes["options"].([]interface{}) { + checked := " " + if form.Get(fmt.Sprintf("form-field-%s-%d", field.Id, i)) == "on" { + checked = "x" + } + result += fmt.Sprintf("- [%s] %s\n", checked, option.(map[interface{}]interface{})["label"]) + } + result += "\n" + } + } + } + + return result, nil +} + // NewIssuePost response for creating new issue func NewIssuePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateIssueForm) @@ -1031,6 +1127,16 @@ 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 { + return + } + if issueContents == "" { + // Not a form + issueContents = form.Content + } + issue := &issues_model.Issue{ RepoID: repo.ID, Repo: repo, @@ -1038,7 +1144,7 @@ func NewIssuePost(ctx *context.Context) { PosterID: ctx.Doer.ID, Poster: ctx.Doer, MilestoneID: milestoneID, - Content: form.Content, + Content: issueContents, Ref: form.Ref, } diff --git a/templates/repo/issue/comment_tab.tmpl b/templates/repo/issue/comment_tab.tmpl index c1ca69dfb3e2f..68cf82e8532a4 100644 --- a/templates/repo/issue/comment_tab.tmpl +++ b/templates/repo/issue/comment_tab.tmpl @@ -1,19 +1,71 @@ - -
-
- +{{- if .IssueFormTemplate}} + + {{range $field_idx, $field := .IssueFormTemplate.Fields}} + {{- if $field.Attributes.label}}

{{$field.Attributes.label}}

{{end}} + {{- if $field.Attributes.description}}
{{RenderMarkdownToHtml $field.Attributes.description}}
{{end}} + + {{- if eq .Type "markdown"}} +
{{RenderMarkdownToHtml $field.Attributes.value}}
+ {{else if eq .Type "input"}} + + {{else if eq .Type "textarea"}} + {{- if .Attributes.render}} + + {{end}} + + {{else if eq .Type "dropdown"}} + {{/* TODO: work with .Attribtes.multiple */}} + + {{else if eq .Type "checkboxes"}} + {{range $chk_id, $chk := $field.Attributes.options}} +
+ {{end}} + {{end}} + + {{end}} +{{else}} + -
- {{.locale.Tr "loading"}} -
-
-{{if .IsAttachmentEnabled}}
- {{template "repo/upload" .}} +
+ +
+
+ {{.locale.Tr "loading"}} +
+ {{if .IsAttachmentEnabled}} +
+ {{template "repo/upload" .}} +
+ {{end}} {{end}} diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 1f03014b542bb..7a7b5291685cf 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -1,3 +1,9 @@ +{{- if .IssueFormTemplate}} +

{{.locale.Tr "issue"}}: {{.IssueFormTemplate.Name}}

+ {{.IssueFormTemplate.About}} +
+
+{{end}}
{{.CsrfTokenHtml}} {{if .Flash}} From 88bf8b8e53fdadd59239bdb74071e536f7ce0e6b Mon Sep 17 00:00:00 2001 From: David Shlemayev Date: Sat, 13 Aug 2022 04:28:52 +0300 Subject: [PATCH 3/8] Fix lint issues --- modules/context/repo.go | 10 +++++--- modules/structs/issue_form.go | 4 +-- routers/web/repo/issue.go | 36 ++++++++++++--------------- templates/repo/issue/comment_tab.tmpl | 18 +++++++------- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/modules/context/repo.go b/modules/context/repo.go index b7768efe14f7c..4046ce48926be 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -8,7 +8,6 @@ package context import ( "context" "fmt" - "gopkg.in/yaml.v2" "html" "io" "net/http" @@ -16,6 +15,8 @@ import ( "path" "strings" + "gopkg.in/yaml.v2" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -1033,8 +1034,9 @@ func UnitTypes() func(ctx *Context) { } } -func ExtractTemplateFromYaml(templateContent []byte, meta *api.IssueTemplate) (tmpl *api.IssueFormTemplate, err error) { - err = yaml.Unmarshal(templateContent, &tmpl) +func ExtractTemplateFromYaml(templateContent []byte, meta *api.IssueTemplate) (*api.IssueFormTemplate, error) { + var tmpl *api.IssueFormTemplate + err := yaml.Unmarshal(templateContent, &tmpl) if err != nil { return nil, err } @@ -1049,7 +1051,7 @@ func ExtractTemplateFromYaml(templateContent []byte, meta *api.IssueTemplate) (t meta.Ref = tmpl.Ref } - return + return tmpl, nil } // IssueTemplatesFromDefaultBranch checks for issue templates in the repo's default branch diff --git a/modules/structs/issue_form.go b/modules/structs/issue_form.go index 303062ead5385..4c863f4acee84 100644 --- a/modules/structs/issue_form.go +++ b/modules/structs/issue_form.go @@ -8,7 +8,7 @@ import "strings" type FormField struct { Type string `yaml:"type"` - Id string `yaml:"id"` + ID string `yaml:"id"` Attributes map[string]interface{} `yaml:"attributes"` Validations map[string]interface{} `yaml:"validations"` } @@ -33,7 +33,7 @@ func (it IssueFormTemplate) Valid() bool { } for _, field := range it.Fields { - if strings.TrimSpace(field.Id) == "" { + if strings.TrimSpace(field.ID) == "" { // TODO: add IDs should be optional, maybe generate slug from label? or use numberic id return false } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index fe48c14b6b8d2..8fdcdb2a1e501 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -751,12 +751,7 @@ func getFileContentFromDefaultBranch(repo *context.Repository, filename string) return string(bytes), true } -func getTemplate(repo *context.Repository, template string, possibleDirs, possibleFiles []string) ( - outMeta *api.IssueTemplate, - outTemplateBody string, - outFormTemplateBody *api.IssueFormTemplate, - err error, -) { +func getTemplate(repo *context.Repository, template string, possibleDirs, possibleFiles []string) (*api.IssueTemplate, string, *api.IssueFormTemplate, error) { // Add `possibleFiles` and each `{possibleDirs}/{template}` to `templateCandidates` templateCandidates := make([]string, 0, len(possibleFiles)) if template != "" { @@ -771,29 +766,31 @@ func getTemplate(repo *context.Repository, template string, possibleDirs, possib templateContent, found := getFileContentFromDefaultBranch(repo, filename) if found { meta := api.IssueTemplate{FileName: filename} + var templateBody string + var formTemplateBody *api.IssueFormTemplate + var err error if strings.HasSuffix(filename, ".md") { // Parse markdown template - outTemplateBody, err = markdown.ExtractMetadata(templateContent, meta) + templateBody, err = markdown.ExtractMetadata(templateContent, meta) } else if strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml") { // Parse yaml (form) template - outFormTemplateBody, err = context.ExtractTemplateFromYaml([]byte(templateContent), &meta) - outFormTemplateBody.FileName = path.Base(filename) + formTemplateBody, err = context.ExtractTemplateFromYaml([]byte(templateContent), &meta) + formTemplateBody.FileName = path.Base(filename) } else { err = errors.New("invalid template type") } if err != nil { log.Debug("could not extract metadata from %s [%s]: %v", filename, repo.Repository.FullName(), err) - outTemplateBody = templateContent + templateBody = templateContent err = nil } - outMeta = &meta - return + return &meta, templateBody, formTemplateBody, err } } - err = errors.New("no template found") - return + + return nil, "", nil, errors.New("no template found") } func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) { @@ -833,7 +830,6 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, ctx.Data["label_ids"] = strings.Join(labelIDs, ",") ctx.Data["Reference"] = templateMeta.Ref ctx.Data["RefEndName"] = git.RefEndName(templateMeta.Ref) - return } // NewIssue render creating issue page @@ -1059,11 +1055,11 @@ func renderIssueFormValues(ctx *context.Context, form *url.Values) (string, erro // Render values result := "" for _, field := range formTemplateBody.Fields { - if field.Id != "" { + if field.ID != "" { // Get field label label := field.Attributes["label"] if label == "" { - label = field.Id + label = field.ID } // Format the value into Markdown @@ -1072,15 +1068,15 @@ func renderIssueFormValues(ctx *context.Context, form *url.Values) (string, erro // Markdown blocks do not appear in output case "input", "textarea", "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)) + 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)) + result += fmt.Sprintf("### %s\n%s\n\n", label, form.Get("form-field-"+field.ID)) } case "checkboxes": result += fmt.Sprintf("### %s\n", label) for i, option := range field.Attributes["options"].([]interface{}) { checked := " " - if form.Get(fmt.Sprintf("form-field-%s-%d", field.Id, i)) == "on" { + if form.Get(fmt.Sprintf("form-field-%s-%d", field.ID, i)) == "on" { checked = "x" } result += fmt.Sprintf("- [%s] %s\n", checked, option.(map[interface{}]interface{})["label"]) diff --git a/templates/repo/issue/comment_tab.tmpl b/templates/repo/issue/comment_tab.tmpl index 68cf82e8532a4..689f42c141f31 100644 --- a/templates/repo/issue/comment_tab.tmpl +++ b/templates/repo/issue/comment_tab.tmpl @@ -9,26 +9,26 @@ {{else if eq .Type "input"}} {{else if eq .Type "textarea"}} {{- if .Attributes.render}} - + {{end}} {{else if eq .Type "dropdown"}} {{/* TODO: work with .Attribtes.multiple */}} {{.label}}{{if .required}} *{{end}} From aa4c7a1c9f5f840c41382b7a1b3a3418be83c057 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 13 Aug 2022 16:20:57 +0100 Subject: [PATCH 4/8] fix lint Signed-off-by: Andrew Thornton --- modules/context/repo.go | 3 +-- templates/repo/issue/comment_tab.tmpl | 32 +++++++++++++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/modules/context/repo.go b/modules/context/repo.go index 4046ce48926be..d0302577f272f 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -15,8 +15,6 @@ import ( "path" "strings" - "gopkg.in/yaml.v2" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -37,6 +35,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 diff --git a/templates/repo/issue/comment_tab.tmpl b/templates/repo/issue/comment_tab.tmpl index 689f42c141f31..dc120bb06e5df 100644 --- a/templates/repo/issue/comment_tab.tmpl +++ b/templates/repo/issue/comment_tab.tmpl @@ -2,28 +2,28 @@ {{range $field_idx, $field := .IssueFormTemplate.Fields}} {{- if $field.Attributes.label}}

{{$field.Attributes.label}}

{{end}} - {{- if $field.Attributes.description}}
{{RenderMarkdownToHtml $field.Attributes.description}}
{{end}} + {{- if $field.Attributes.description}}
{{RenderMarkdownToHtml $field.Attributes.description}}
{{end}} - {{- if eq .Type "markdown"}} + {{- if eq .Type "markdown"}}
{{RenderMarkdownToHtml $field.Attributes.value}}
{{else if eq .Type "input"}} + aria-labelledby="form-label-{{$field_idx}}" + name="form-field-{{$field.ID}}" + id="form-field-{{$field.ID}}" + placeholder="{{$field.Attributes.placeholder}}" + value="{{$field.Attributes.value}}" + {{- if .Validations.required}}required{{end}} /> {{else if eq .Type "textarea"}} {{- if .Attributes.render}} {{end}} + id="form-field-{{$field.ID}}" + placeholder="{{$field.Attributes.placeholder}}" + {{- if .Attributes.render}}class="no-easymde"{{end}} + {{- if .Validations.required}}required{{end}}>{{$field.Attributes.value}} {{else if eq .Type "dropdown"}} {{/* TODO: work with .Attribtes.multiple */}} + name="form-field-{{$field.ID}}-{{$chk_id}}" + id="form-field-{{$field.ID}}-{{$chk_id}}" + {{if .checked}}checked{{end}} + {{if .required}}required{{end}} /> {{.label}}{{if .required}} *{{end}}
{{end}} From 532fa06cd79f1af30f80cb54a70546df0fd6732b Mon Sep 17 00:00:00 2001 From: David Shlemayev Date: Sun, 14 Aug 2022 01:52:43 +0300 Subject: [PATCH 5/8] Display template validation errors in issue template picker + Make markdown template parse errors be hard errors --- modules/context/repo.go | 41 ++++++++++++----- modules/structs/issue_form.go | 77 +++++++++++++++++++++++++++----- routers/api/v1/repo/repo.go | 3 +- routers/web/repo/issue.go | 46 ++++++++++++------- routers/web/repo/milestone.go | 3 +- templates/repo/issue/choose.tmpl | 14 ++++++ 6 files changed, 147 insertions(+), 37 deletions(-) diff --git a/modules/context/repo.go b/modules/context/repo.go index d0302577f272f..87b6c740ab030 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -13,6 +13,7 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" "code.gitea.io/gitea/models" @@ -1033,11 +1034,23 @@ func UnitTypes() func(ctx *Context) { } } -func ExtractTemplateFromYaml(templateContent []byte, meta *api.IssueTemplate) (*api.IssueFormTemplate, error) { +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, err + 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 @@ -1050,22 +1063,23 @@ func ExtractTemplateFromYaml(templateContent []byte, meta *api.IssueTemplate) (* meta.Ref = tmpl.Ref } - return tmpl, nil + 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 } } @@ -1076,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") { @@ -1111,6 +1125,8 @@ func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate { 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 { @@ -1137,9 +1153,14 @@ func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate { var it api.IssueTemplate it.FileName = path.Base(entry.Name()) - _, err = ExtractTemplateFromYaml(templateContent, &it) + + 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() { @@ -1148,8 +1169,8 @@ func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate { } } if len(issueTemplates) > 0 { - return issueTemplates + return issueTemplates, validationErrs } } - return issueTemplates + return issueTemplates, validationErrs } diff --git a/modules/structs/issue_form.go b/modules/structs/issue_form.go index 4c863f4acee84..ae7dfa2ce9af1 100644 --- a/modules/structs/issue_form.go +++ b/modules/structs/issue_form.go @@ -4,7 +4,10 @@ package structs -import "strings" +import ( + "fmt" + "strings" +) type FormField struct { Type string `yaml:"type"` @@ -26,18 +29,72 @@ type IssueFormTemplate struct { FileName string `yaml:"-"` } -// Valid checks whether an IssueFormTemplate is considered valid, e.g. at least name and about -func (it IssueFormTemplate) Valid() bool { - if strings.TrimSpace(it.Name) == "" || strings.TrimSpace(it.About) == "" { - return false +// 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") } - for _, field := range it.Fields { - if strings.TrimSpace(field.ID) == "" { - // TODO: add IDs should be optional, maybe generate slug from label? or use numberic id - return false + // 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 true + return errs } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index cdd1f7d5c4a7c..f8cf75f719aba 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -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) } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 8fdcdb2a1e501..79a3f763ccfbf 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -65,6 +65,7 @@ const ( issueTemplateKey = "IssueTemplate" issueFormTemplateKey = "IssueFormTemplate" + issueFormErrorsKey = "IssueTemplateErrors" issueTemplateTitleKey = "IssueTemplateTitle" ) @@ -408,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)) @@ -751,7 +753,9 @@ func getFileContentFromDefaultBranch(repo *context.Repository, filename string) return string(bytes), true } -func getTemplate(repo *context.Repository, template string, possibleDirs, possibleFiles []string) (*api.IssueTemplate, string, *api.IssueFormTemplate, error) { +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 template != "" { @@ -772,29 +776,32 @@ func getTemplate(repo *context.Repository, template string, possibleDirs, possib if strings.HasSuffix(filename, ".md") { // Parse markdown template - templateBody, err = markdown.ExtractMetadata(templateContent, meta) + templateBody, err = markdown.ExtractMetadata(templateContent, &meta) } else if strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml") { // Parse yaml (form) template - formTemplateBody, err = context.ExtractTemplateFromYaml([]byte(templateContent), &meta) - formTemplateBody.FileName = path.Base(filename) + 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, repo.Repository.FullName(), err) - templateBody = templateContent - err = nil } - return &meta, templateBody, formTemplateBody, err + return &meta, templateBody, formTemplateBody, validationErrs, err } } - return nil, "", nil, errors.New("no template found") + return nil, "", nil, validationErrs, errors.New("no template found") } func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) { - templateMeta, templateBody, formTemplateBody, err := getTemplate(ctx.Repo, ctx.FormString("template"), possibleDirs, possibleFiles) + templateMeta, templateBody, formTemplateBody, validationErrs, err := getTemplate(ctx.Repo, ctx.FormString("template"), possibleDirs, possibleFiles) if err != nil { return } @@ -802,6 +809,9 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, if formTemplateBody != nil { ctx.Data[issueFormTemplateKey] = formTemplateBody } + if validationErrs != nil && len(validationErrs) > 0 { + ctx.Data[issueFormErrorsKey] = validationErrs + } ctx.Data[issueTemplateTitleKey] = templateMeta.Title ctx.Data[ctxDataKey] = templateBody @@ -836,7 +846,8 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, 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") @@ -893,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 validationErrs != nil && len(validationErrs) > 0 { + ctx.Data[issueFormErrorsKey] = validationErrs + } ctx.Data["IssueTemplates"] = issueTemplates if len(issueTemplates) == 0 { @@ -1039,7 +1053,7 @@ func renderIssueFormValues(ctx *context.Context, form *url.Values) (string, erro } // Fetch template - _, _, formTemplateBody, err := getTemplate( + _, _, formTemplateBody, _, err := getTemplate( ctx.Repo, form.Get("form-type"), context.IssueTemplateDirCandidates, @@ -1094,7 +1108,8 @@ 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") @@ -1287,7 +1302,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) { diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 1e75bd79fb27c..607fe63461858 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -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) diff --git a/templates/repo/issue/choose.tmpl b/templates/repo/issue/choose.tmpl index bbddd6d9a6ce3..a3d4975f0f498 100644 --- a/templates/repo/issue/choose.tmpl +++ b/templates/repo/issue/choose.tmpl @@ -30,6 +30,20 @@
+ {{- if .IssueTemplateErrors}} +
+
+
The following issue templates have errors:
+
    + {{range $filename, $errors := .IssueTemplateErrors}} + {{range $errors}} +
  • {{$filename}}: {{.}}
  • + {{end}} + {{end}} +
+
+
+ {{end}} {{template "base/footer" .}} From 1eb9fce33587d009ab95c6b8a60f8adcce4d63af Mon Sep 17 00:00:00 2001 From: David Shlemayev Date: Sun, 14 Aug 2022 01:59:49 +0300 Subject: [PATCH 6/8] Fix lint errors --- routers/web/repo/issue.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 79a3f763ccfbf..bcf0ff529d942 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -809,7 +809,7 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, if formTemplateBody != nil { ctx.Data[issueFormTemplateKey] = formTemplateBody } - if validationErrs != nil && len(validationErrs) > 0 { + if len(validationErrs) > 0 { ctx.Data[issueFormErrorsKey] = validationErrs } @@ -905,7 +905,7 @@ func NewIssueChooseTemplate(ctx *context.Context) { ctx.Data["PageIsIssueList"] = true issueTemplates, validationErrs := ctx.IssueTemplatesFromDefaultBranch() - if validationErrs != nil && len(validationErrs) > 0 { + if len(validationErrs) > 0 { ctx.Data[issueFormErrorsKey] = validationErrs } ctx.Data["IssueTemplates"] = issueTemplates From b021d9fa63372c27209b59891a80143fbdb0aa6d Mon Sep 17 00:00:00 2001 From: David Shlemayev Date: Sun, 14 Aug 2022 02:05:35 +0300 Subject: [PATCH 7/8] Fix lint errors 2 --- templates/repo/issue/choose.tmpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/repo/issue/choose.tmpl b/templates/repo/issue/choose.tmpl index a3d4975f0f498..c03a308f701ae 100644 --- a/templates/repo/issue/choose.tmpl +++ b/templates/repo/issue/choose.tmpl @@ -35,11 +35,11 @@
The following issue templates have errors:
    - {{range $filename, $errors := .IssueTemplateErrors}} - {{range $errors}} + {{range $filename, $errors := .IssueTemplateErrors}} + {{range $errors}}
  • {{$filename}}: {{.}}
  • - {{end}} - {{end}} + {{end}} + {{end}}
From 4f44a256126af7500e25b553bd8df3114bcc3bde Mon Sep 17 00:00:00 2001 From: David Shlemayev Date: Sun, 21 Aug 2022 00:30:26 +0300 Subject: [PATCH 8/8] Fix tabindex, make dropdown+multiple be checkboxes --- options/locale/locale_en-US.ini | 1 + routers/web/repo/issue.go | 43 +++++++++++++++++++-------- templates/repo/issue/comment_tab.tmpl | 39 ++++++++++++++++-------- templates/repo/issue/new_form.tmpl | 2 +- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 39874122098d9..f0ab830e7afea 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1203,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 diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index bcf0ff529d942..910e2b18ae2cc 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1077,25 +1077,39 @@ func renderIssueFormValues(ctx *context.Context, form *url.Values) (string, erro } // Format the value into Markdown - switch field.Type { - case "markdown": + if field.Type == "markdown" { // Markdown blocks do not appear in output - case "input", "textarea", "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)) - } - case "checkboxes": + } 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{}) { - checked := " " - if form.Get(fmt.Sprintf("form-field-%s-%d", field.ID, i)) == "on" { - checked = "x" + // 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", checked, option.(map[interface{}]interface{})["label"]) + 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)) } } } @@ -1141,6 +1155,9 @@ func NewIssuePost(ctx *context.Context) { // 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 == "" { diff --git a/templates/repo/issue/comment_tab.tmpl b/templates/repo/issue/comment_tab.tmpl index dc120bb06e5df..05f6886eb2cfa 100644 --- a/templates/repo/issue/comment_tab.tmpl +++ b/templates/repo/issue/comment_tab.tmpl @@ -1,7 +1,7 @@ {{- if .IssueFormTemplate}} {{range $field_idx, $field := .IssueFormTemplate.Fields}} - {{- if $field.Attributes.label}}

{{$field.Attributes.label}}

{{end}} + {{- if $field.Attributes.label}}

{{$field.Attributes.label}} {{if $field.Validations.required}} *{{end}}

{{end}} {{- if $field.Attributes.description}}
{{RenderMarkdownToHtml $field.Attributes.description}}
{{end}} {{- if eq .Type "markdown"}} @@ -12,6 +12,7 @@ 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"}} @@ -22,29 +23,41 @@ 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}} + {{else if eq .Type "checkboxes"}} + {{range $chk_id, $chk := $field.Attributes.options}} +
+ {{end}} + {{else if and (eq .Type "dropdown") $field.Attributes.multiple}} + {{range $chk_id, $chk := $field.Attributes.options}} +
+ {{end}} {{else if eq .Type "dropdown"}} - {{/* TODO: work with .Attribtes.multiple */}} - {{else if eq .Type "checkboxes"}} - {{range $chk_id, $chk := $field.Attributes.options}} -
- {{end}} {{end}} {{end}} diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 7a7b5291685cf..d343829c5ac49 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -4,7 +4,7 @@

{{end}} - + {{.CsrfTokenHtml}} {{if .Flash}}