From 77e29e0c39392f142627303bd798fb55258072b2 Mon Sep 17 00:00:00 2001 From: 6543 Date: Mon, 4 Mar 2024 01:37:00 +0100 Subject: 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 Co-authored-by: delvh --- modules/issue/template/template.go | 69 ++++++++++++++++++-- modules/issue/template/template_test.go | 109 ++++++++++++++++++++++++++------ modules/issue/template/unmarshal.go | 9 +++ 3 files changed, 162 insertions(+), 25 deletions(-) (limited to 'modules/issue/template') 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" ) @@ -318,6 +319,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: ` @@ -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,9 +684,14 @@ 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 @@ -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) + } + } } } -- cgit v1.2.3