aboutsummaryrefslogtreecommitdiffstats
path: root/modules/issue/template
diff options
context:
space:
mode:
author6543 <m.huber@kithara.com>2024-03-04 01:37:00 +0100
committerGitHub <noreply@github.com>2024-03-04 01:37:00 +0100
commit77e29e0c39392f142627303bd798fb55258072b2 (patch)
treefe2d996c1a569688a280f677690581e06ff3eb22 /modules/issue/template
parent2fb917f69e59f8b75825bf4fe659856b9dd02f44 (diff)
downloadgitea-77e29e0c39392f142627303bd798fb55258072b2.tar.gz
gitea-77e29e0c39392f142627303bd798fb55258072b2.zip
Extend issue template yaml engine (#29274)
Add new option: `visible`: witch can hide a specific field of the form or the created content afterwards It is a string array witch can contain `form` and `content`. If only `form` is present, it wont show up in the created issue afterwards and the other way around. By default it sets both except for markdown As they are optional and github don't have any similar thing, it is non breaking and also do not conflict with it. With this you can: - define "post issue creation" elements like a TODO list to track an issue state - make sure to have a checkbox that reminds the user to check for a thing but dont have it in the created issue afterwards - define markdown for the created issue (was the downside of using yaml instead of md in the past) - ... ## Demo ```yaml name: New Contribution description: External Contributor creating a pull body: - type: checkboxes id: extern-todo visible: [form] attributes: label: Contribution Guidelines options: - label: I checked there exist no similar feature to be extended required: true - label: I did read the CONTRIBUTION.MD required: true - type: checkboxes id: intern-todo visible: [content] attributes: label: Maintainer Check-List options: - label: Does this pull follow the KISS principe - label: Checked if internal bord was notifyed # .... ``` [Demo Video](https://cloud.obermui.de/s/tm34fSAbJp9qw9z/download/vid-20240220-152751.mkv) --- *Sponsored by Kithara Software GmbH* --------- Co-authored-by: John Olheiser <john.olheiser@gmail.com> Co-authored-by: delvh <dev.lh@web.de>
Diffstat (limited to 'modules/issue/template')
-rw-r--r--modules/issue/template/template.go69
-rw-r--r--modules/issue/template/template_test.go109
-rw-r--r--modules/issue/template/unmarshal.go9
3 files changed, 162 insertions, 25 deletions
diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go
index 4e813fc91f..3be48b9edc 100644
--- a/modules/issue/template/template.go
+++ b/modules/issue/template/template.go
@@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error {
// The label is not required for a markdown or checkboxes field
return nil
}
- return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required")
+ if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil {
+ return err
+ }
+ if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() {
+ return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field")
+ }
+ return nil
}
func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error {
@@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error {
return position.Errorf("'label' is required and should be a string")
}
+ if visibility, ok := opt["visible"]; ok {
+ visibilityList, ok := visibility.([]any)
+ if !ok {
+ return position.Errorf("'visible' should be list")
+ }
+ for _, visibleType := range visibilityList {
+ visibleType, ok := visibleType.(string)
+ if !ok || !(visibleType == "form" || visibleType == "content") {
+ return position.Errorf("'visible' list can only contain strings of 'form' and 'content'")
+ }
+ }
+ }
+
if required, ok := opt["required"]; ok {
if _, ok := required.(bool); !ok {
return position.Errorf("'required' should be a bool")
}
+
+ // validate if hidden field is required
+ if visibility, ok := opt["visible"]; ok {
+ visibilityList, _ := visibility.([]any)
+ isVisible := false
+ for _, v := range visibilityList {
+ if vv, _ := v.(string); vv == "form" {
+ isVisible = true
+ break
+ }
+ }
+ if !isVisible {
+ return position.Errorf("can not require a hidden checkbox")
+ }
+ }
}
}
}
@@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string {
IssueFormField: field,
Values: values,
}
- if f.ID == "" {
+ if f.ID == "" || !f.VisibleInContent() {
continue
}
f.WriteTo(builder)
@@ -253,11 +287,6 @@ type valuedField struct {
}
func (f *valuedField) WriteTo(builder *strings.Builder) {
- if f.Type == api.IssueFormFieldTypeMarkdown {
- // markdown blocks do not appear in output
- return
- }
-
// write label
if !f.HideLabel() {
_, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label())
@@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
switch f.Type {
case api.IssueFormFieldTypeCheckboxes:
for _, option := range f.Options() {
+ if !option.VisibleInContent() {
+ continue
+ }
checked := " "
if option.IsChecked() {
checked = "x"
@@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
} else {
_, _ = fmt.Fprintf(builder, "%s\n", value)
}
+ case api.IssueFormFieldTypeMarkdown:
+ if value, ok := f.Attributes["value"].(string); ok {
+ _, _ = fmt.Fprintf(builder, "%s\n", value)
+ }
}
_, _ = fmt.Fprintln(builder)
}
@@ -314,6 +350,9 @@ func (f *valuedField) Label() string {
}
func (f *valuedField) HideLabel() bool {
+ if f.Type == api.IssueFormFieldTypeMarkdown {
+ return true
+ }
if label, ok := f.Attributes["hide_label"].(bool); ok {
return label
}
@@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool {
return false
}
+func (o *valuedOption) VisibleInContent() bool {
+ if o.field.Type == api.IssueFormFieldTypeCheckboxes {
+ if vs, ok := o.data.(map[string]any); ok {
+ if vl, ok := vs["visible"].([]any); ok {
+ for _, v := range vl {
+ if vv, _ := v.(string); vv == "content" {
+ return true
+ }
+ }
+ return false
+ }
+ }
+ }
+ return true
+}
+
var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}")
// minQuotes return 3 or more back-quotes.
diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go
index 06e6b70d35..e24b962d61 100644
--- a/modules/issue/template/template_test.go
+++ b/modules/issue/template/template_test.go
@@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -319,6 +320,42 @@ body:
wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
},
{
+ name: "field is required but hidden",
+ content: `
+name: "test"
+about: "this is about"
+body:
+ - type: "input"
+ id: "1"
+ attributes:
+ label: "a"
+ validations:
+ required: true
+ visible: [content]
+`,
+ wantErr: "body[0](input): can not require a hidden field",
+ },
+ {
+ name: "checkboxes is required but hidden",
+ content: `
+name: "test"
+about: "this is about"
+body:
+ - type: checkboxes
+ id: "1"
+ attributes:
+ label: Label of checkboxes
+ description: Description of checkboxes
+ options:
+ - label: Option 1
+ required: false
+ - label: Required and hidden
+ required: true
+ visible: [content]
+`,
+ wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
+ },
+ {
name: "valid",
content: `
name: Name
@@ -374,8 +411,11 @@ body:
required: true
- label: Option 2 of checkboxes
required: false
- - label: Option 3 of checkboxes
+ - label: Hidden Option 3 of checkboxes
+ visible: [content]
+ - label: Required but not submitted
required: true
+ visible: [form]
`,
want: &api.IssueTemplate{
Name: "Name",
@@ -390,6 +430,7 @@ body:
Attributes: map[string]any{
"value": "Value of the markdown",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
{
Type: "textarea",
@@ -404,6 +445,7 @@ body:
Validations: map[string]any{
"required": true,
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "input",
@@ -419,6 +461,7 @@ body:
"is_number": true,
"regex": "[a-zA-Z0-9]+",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "dropdown",
@@ -436,6 +479,7 @@ body:
Validations: map[string]any{
"required": true,
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "checkboxes",
@@ -446,9 +490,11 @@ body:
"options": []any{
map[string]any{"label": "Option 1 of checkboxes", "required": true},
map[string]any{"label": "Option 2 of checkboxes", "required": false},
- map[string]any{"label": "Option 3 of checkboxes", "required": true},
+ map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}},
+ map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}},
},
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
},
FileName: "test.yaml",
@@ -467,7 +513,12 @@ body:
- type: markdown
id: id1
attributes:
- value: Value of the markdown
+ value: Value of the markdown shown in form
+ - type: markdown
+ id: id2
+ attributes:
+ value: Value of the markdown shown in created issue
+ visible: [content]
`,
want: &api.IssueTemplate{
Name: "Name",
@@ -480,8 +531,17 @@ body:
Type: "markdown",
ID: "id1",
Attributes: map[string]any{
- "value": "Value of the markdown",
+ "value": "Value of the markdown shown in form",
+ },
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
+ },
+ {
+ Type: "markdown",
+ ID: "id2",
+ Attributes: map[string]any{
+ "value": "Value of the markdown shown in created issue",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent},
},
},
FileName: "test.yaml",
@@ -515,6 +575,7 @@ body:
Attributes: map[string]any{
"value": "Value of the markdown",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
},
FileName: "test.yaml",
@@ -548,6 +609,7 @@ body:
Attributes: map[string]any{
"value": "Value of the markdown",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
},
FileName: "test.yaml",
@@ -622,10 +684,15 @@ body:
- type: markdown
id: id1
attributes:
- value: Value of the markdown
- - type: textarea
+ value: Value of the markdown shown in form
+ - type: markdown
id: id2
attributes:
+ value: Value of the markdown shown in created issue
+ visible: [content]
+ - type: textarea
+ id: id3
+ attributes:
label: Label of textarea
description: Description of textarea
placeholder: Placeholder of textarea
@@ -634,7 +701,7 @@ body:
validations:
required: true
- type: input
- id: id3
+ id: id4
attributes:
label: Label of input
description: Description of input
@@ -646,7 +713,7 @@ body:
is_number: true
regex: "[a-zA-Z0-9]+"
- type: dropdown
- id: id4
+ id: id5
attributes:
label: Label of dropdown
description: Description of dropdown
@@ -658,7 +725,7 @@ body:
validations:
required: true
- type: checkboxes
- id: id5
+ id: id6
attributes:
label: Label of checkboxes
description: Description of checkboxes
@@ -669,20 +736,26 @@ body:
required: false
- label: Option 3 of checkboxes
required: true
+ visible: [form]
+ - label: Hidden Option of checkboxes
+ visible: [content]
`,
values: map[string][]string{
- "form-field-id2": {"Value of id2"},
"form-field-id3": {"Value of id3"},
- "form-field-id4": {"0,1"},
- "form-field-id5-0": {"on"},
- "form-field-id5-2": {"on"},
+ "form-field-id4": {"Value of id4"},
+ "form-field-id5": {"0,1"},
+ "form-field-id6-0": {"on"},
+ "form-field-id6-2": {"on"},
},
},
- want: `### Label of textarea
-` + "```bash\nValue of id2\n```" + `
+ want: `Value of the markdown shown in created issue
+
+### Label of textarea
+
+` + "```bash\nValue of id3\n```" + `
-Value of id3
+Value of id4
### Label of dropdown
@@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown
- [x] Option 1 of checkboxes
- [ ] Option 2 of checkboxes
-- [x] Option 3 of checkboxes
+- [ ] Hidden Option of checkboxes
`,
},
@@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown
t.Fatal(err)
}
if got := RenderToMarkdown(template, tt.args.values); got != tt.want {
- t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want)
+ assert.EqualValues(t, tt.want, got)
}
})
}
diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go
index 8cae8d4c42..0fc13d7ddf 100644
--- a/modules/issue/template/unmarshal.go
+++ b/modules/issue/template/unmarshal.go
@@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
}
}
for i, v := range it.Fields {
+ // set default id value
if v.ID == "" {
v.ID = strconv.Itoa(i)
}
+ // set default visibility
+ if v.Visible == nil {
+ v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}
+ // markdown is not submitted by default
+ if v.Type != api.IssueFormFieldTypeMarkdown {
+ v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent)
+ }
+ }
}
}