diff options
author | 6543 <m.huber@kithara.com> | 2024-03-04 01:37:00 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-04 01:37:00 +0100 |
commit | 77e29e0c39392f142627303bd798fb55258072b2 (patch) | |
tree | fe2d996c1a569688a280f677690581e06ff3eb22 /modules/issue/template | |
parent | 2fb917f69e59f8b75825bf4fe659856b9dd02f44 (diff) | |
download | gitea-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.go | 69 | ||||
-rw-r--r-- | modules/issue/template/template_test.go | 109 | ||||
-rw-r--r-- | modules/issue/template/unmarshal.go | 9 |
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) + } + } } } |