From 0a2bed053f8fb3c03d7296df67cb2c8238ba7cb3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 6 Sep 2023 07:47:27 +0100 Subject: [PATCH 1/5] feat: Allow custom field values to be specified as JSON #ankitpokhrel/jira-cli/593 --- internal/cmdcommon/create.go | 3 +++ pkg/jira/create.go | 10 ++++++++++ pkg/jira/customfield.go | 8 ++++++++ pkg/jira/edit.go | 10 ++++++++++ 4 files changed, 31 insertions(+) diff --git a/internal/cmdcommon/create.go b/internal/cmdcommon/create.go index 47ef9933..77a1e889 100644 --- a/internal/cmdcommon/create.go +++ b/internal/cmdcommon/create.go @@ -252,6 +252,9 @@ func ValidateCustomFields(fields map[string]string, configuredFields []jira.Issu invalidCustomFields := make([]string, 0, len(fields)) for key := range fields { + if strings.HasPrefix(key, "json:") { + key = key[5:] + } if _, ok := fieldsMap[key]; !ok { invalidCustomFields = append(invalidCustomFields, key) } diff --git a/pkg/jira/create.go b/pkg/jira/create.go index 0f4edbd7..697cc9af 100644 --- a/pkg/jira/create.go +++ b/pkg/jira/create.go @@ -228,12 +228,22 @@ func constructCustomFields(fields map[string]string, configuredFields []IssueTyp data.Fields.M.customFields = make(customField) for key, val := range fields { + rawJson := false + if strings.HasPrefix(key, "json:") { + key = key[5:] + rawJson = true + } for _, configured := range configuredFields { identifier := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(configured.Name)), " ", "-") if identifier != strings.ToLower(key) { continue } + if rawJson { + data.Fields.M.customFields[configured.Key] = customFieldTypeJson{Json: val} + continue + } + switch configured.Schema.DataType { case customFieldFormatOption: data.Fields.M.customFields[configured.Key] = customFieldTypeOption{Value: val} diff --git a/pkg/jira/customfield.go b/pkg/jira/customfield.go index 92f3c675..08953137 100644 --- a/pkg/jira/customfield.go +++ b/pkg/jira/customfield.go @@ -39,3 +39,11 @@ type customFieldTypeProject struct { type customFieldTypeProjectSet struct { Set customFieldTypeProject `json:"set"` } + +type customFieldTypeJson struct { + Json string +} + +func (field customFieldTypeJson) MarshalJSON() ([]byte, error) { + return []byte(field.Json), nil +} diff --git a/pkg/jira/edit.go b/pkg/jira/edit.go index 6c96c336..bc61a41f 100644 --- a/pkg/jira/edit.go +++ b/pkg/jira/edit.go @@ -361,12 +361,22 @@ func constructCustomFieldsForEdit(fields map[string]string, configuredFields []I data.Update.M.customFields = make(customField) for key, val := range fields { + rawJson := false + if strings.HasPrefix(key, "json:") { + key = key[5:] + rawJson = true + } for _, configured := range configuredFields { identifier := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(configured.Name)), " ", "-") if identifier != strings.ToLower(key) { continue } + if rawJson { + data.Update.M.customFields[configured.Key] = []customFieldTypeJson{{Json: val}} + continue + } + switch configured.Schema.DataType { case customFieldFormatOption: data.Update.M.customFields[configured.Key] = []customFieldTypeOptionSet{{Set: customFieldTypeOption{Value: val}}} From 3ac82206ce18102afa0f908616399868bc0208e0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 6 Sep 2023 08:32:34 +0100 Subject: [PATCH 2/5] fix: linting --- internal/cmdcommon/create.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/cmdcommon/create.go b/internal/cmdcommon/create.go index 77a1e889..46556930 100644 --- a/internal/cmdcommon/create.go +++ b/internal/cmdcommon/create.go @@ -252,9 +252,7 @@ func ValidateCustomFields(fields map[string]string, configuredFields []jira.Issu invalidCustomFields := make([]string, 0, len(fields)) for key := range fields { - if strings.HasPrefix(key, "json:") { - key = key[5:] - } + key = strings.TrimPrefix(key, "json:") if _, ok := fieldsMap[key]; !ok { invalidCustomFields = append(invalidCustomFields, key) } From 299d08a069b7f409c56718e5fa36e27c6654c0c4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 7 Oct 2023 09:04:15 +0100 Subject: [PATCH 3/5] feat: Configure JSON custom fields in config file instead of using --custom json:name=val flag syntax --- internal/cmdcommon/create.go | 1 - pkg/jira/create.go | 12 ++---------- pkg/jira/customfield.go | 5 +++++ pkg/jira/edit.go | 12 ++---------- 4 files changed, 9 insertions(+), 21 deletions(-) diff --git a/internal/cmdcommon/create.go b/internal/cmdcommon/create.go index 46556930..47ef9933 100644 --- a/internal/cmdcommon/create.go +++ b/internal/cmdcommon/create.go @@ -252,7 +252,6 @@ func ValidateCustomFields(fields map[string]string, configuredFields []jira.Issu invalidCustomFields := make([]string, 0, len(fields)) for key := range fields { - key = strings.TrimPrefix(key, "json:") if _, ok := fieldsMap[key]; !ok { invalidCustomFields = append(invalidCustomFields, key) } diff --git a/pkg/jira/create.go b/pkg/jira/create.go index 697cc9af..b717f64a 100644 --- a/pkg/jira/create.go +++ b/pkg/jira/create.go @@ -228,22 +228,12 @@ func constructCustomFields(fields map[string]string, configuredFields []IssueTyp data.Fields.M.customFields = make(customField) for key, val := range fields { - rawJson := false - if strings.HasPrefix(key, "json:") { - key = key[5:] - rawJson = true - } for _, configured := range configuredFields { identifier := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(configured.Name)), " ", "-") if identifier != strings.ToLower(key) { continue } - if rawJson { - data.Fields.M.customFields[configured.Key] = customFieldTypeJson{Json: val} - continue - } - switch configured.Schema.DataType { case customFieldFormatOption: data.Fields.M.customFields[configured.Key] = customFieldTypeOption{Value: val} @@ -268,6 +258,8 @@ func constructCustomFields(fields map[string]string, configuredFields []IssueTyp } else { data.Fields.M.customFields[configured.Key] = customFieldTypeNumber(num) } + case customFieldFormatJson: + data.Fields.M.customFields[configured.Key] = customFieldTypeJson{Json: val} default: data.Fields.M.customFields[configured.Key] = val } diff --git a/pkg/jira/customfield.go b/pkg/jira/customfield.go index 08953137..9c66dd39 100644 --- a/pkg/jira/customfield.go +++ b/pkg/jira/customfield.go @@ -5,6 +5,7 @@ const ( customFieldFormatArray = "array" customFieldFormatNumber = "number" customFieldFormatProject = "project" + customFieldFormatJson = "json" ) type customField map[string]interface{} @@ -44,6 +45,10 @@ type customFieldTypeJson struct { Json string } +type customFieldTypeJsonSet struct { + Set customFieldTypeJson `json:"set"` +} + func (field customFieldTypeJson) MarshalJSON() ([]byte, error) { return []byte(field.Json), nil } diff --git a/pkg/jira/edit.go b/pkg/jira/edit.go index bc61a41f..405951d2 100644 --- a/pkg/jira/edit.go +++ b/pkg/jira/edit.go @@ -361,22 +361,12 @@ func constructCustomFieldsForEdit(fields map[string]string, configuredFields []I data.Update.M.customFields = make(customField) for key, val := range fields { - rawJson := false - if strings.HasPrefix(key, "json:") { - key = key[5:] - rawJson = true - } for _, configured := range configuredFields { identifier := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(configured.Name)), " ", "-") if identifier != strings.ToLower(key) { continue } - if rawJson { - data.Update.M.customFields[configured.Key] = []customFieldTypeJson{{Json: val}} - continue - } - switch configured.Schema.DataType { case customFieldFormatOption: data.Update.M.customFields[configured.Key] = []customFieldTypeOptionSet{{Set: customFieldTypeOption{Value: val}}} @@ -405,6 +395,8 @@ func constructCustomFieldsForEdit(fields map[string]string, configuredFields []I } else { data.Update.M.customFields[configured.Key] = []customFieldTypeNumberSet{{Set: customFieldTypeNumber(num)}} } + case customFieldFormatJson: + data.Update.M.customFields[configured.Key] = []customFieldTypeJsonSet{{Set: customFieldTypeJson{Json: val}}} default: data.Update.M.customFields[configured.Key] = []customFieldTypeStringSet{{Set: val}} } From fccd6ed4f5462dfa7f225414dae061d869e254bf Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 8 Oct 2023 09:16:12 +0100 Subject: [PATCH 4/5] feat: Handle unknown custom field types as JSON, and support arrays of these types --- pkg/jira/create.go | 32 +--------------- pkg/jira/customfield.go | 82 +++++++++++++++++++++++++++++------------ pkg/jira/edit.go | 43 ++++++--------------- 3 files changed, 71 insertions(+), 86 deletions(-) diff --git a/pkg/jira/create.go b/pkg/jira/create.go index b717f64a..d51ad523 100644 --- a/pkg/jira/create.go +++ b/pkg/jira/create.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "net/http" - "strconv" "strings" "github.com/ankitpokhrel/jira-cli/pkg/adf" @@ -233,36 +232,7 @@ func constructCustomFields(fields map[string]string, configuredFields []IssueTyp if identifier != strings.ToLower(key) { continue } - - switch configured.Schema.DataType { - case customFieldFormatOption: - data.Fields.M.customFields[configured.Key] = customFieldTypeOption{Value: val} - case customFieldFormatProject: - data.Fields.M.customFields[configured.Key] = customFieldTypeProject{Value: val} - case customFieldFormatArray: - pieces := strings.Split(strings.TrimSpace(val), ",") - if configured.Schema.Items == customFieldFormatOption { - items := make([]customFieldTypeOption, 0) - for _, p := range pieces { - items = append(items, customFieldTypeOption{Value: p}) - } - data.Fields.M.customFields[configured.Key] = items - } else { - data.Fields.M.customFields[configured.Key] = pieces - } - case customFieldFormatNumber: - num, err := strconv.ParseFloat(val, 64) //nolint:gomnd - if err != nil { - // Let Jira API handle data type error for now. - data.Fields.M.customFields[configured.Key] = val - } else { - data.Fields.M.customFields[configured.Key] = customFieldTypeNumber(num) - } - case customFieldFormatJson: - data.Fields.M.customFields[configured.Key] = customFieldTypeJson{Json: val} - default: - data.Fields.M.customFields[configured.Key] = val - } + data.Fields.M.customFields[configured.Key] = constructCustomField(configured.Schema.DataType, configured.Schema.Items, val) } } } diff --git a/pkg/jira/customfield.go b/pkg/jira/customfield.go index 9c66dd39..0b299d6d 100644 --- a/pkg/jira/customfield.go +++ b/pkg/jira/customfield.go @@ -1,9 +1,17 @@ package jira +import ( + "encoding/json" + "strconv" + "strings" +) + const ( + customFieldFormatAny = "any" customFieldFormatOption = "option" customFieldFormatArray = "array" customFieldFormatNumber = "number" + customFieldFormatString = "string" customFieldFormatProject = "project" customFieldFormatJson = "json" ) @@ -12,43 +20,69 @@ type customField map[string]interface{} type customFieldTypeNumber float64 -type customFieldTypeNumberSet struct { - Set customFieldTypeNumber `json:"set"` -} - -type customFieldTypeStringSet struct { - Set string `json:"set"` -} +type customFieldTypeString string type customFieldTypeOption struct { Value string `json:"value"` } -type customFieldTypeOptionSet struct { - Set customFieldTypeOption `json:"set"` -} - -type customFieldTypeOptionAddRemove struct { - Add *customFieldTypeOption `json:"add,omitempty"` - Remove *customFieldTypeOption `json:"remove,omitempty"` -} - type customFieldTypeProject struct { Value string `json:"key"` } -type customFieldTypeProjectSet struct { - Set customFieldTypeProject `json:"set"` -} - type customFieldTypeJson struct { Json string } -type customFieldTypeJsonSet struct { - Set customFieldTypeJson `json:"set"` -} - func (field customFieldTypeJson) MarshalJSON() ([]byte, error) { return []byte(field.Json), nil } + +type customFieldEditTypeSet struct { + Set any `json:"set"` +} + +type customFieldEditTypeAddRemove struct { + Add *any `json:"add,omitempty"` + Remove *any `json:"remove,omitempty"` +} + +func constructCustomField(dataType string, itemType string, value string) any { + switch dataType { + case customFieldFormatOption: + return customFieldTypeOption{Value: value} + case customFieldFormatProject: + return customFieldTypeProject{Value: value} + case customFieldFormatArray: + pieces := strings.Split(strings.TrimSpace(value), ",") + items := make([]any, len(pieces)) + for idx, piece := range pieces { + items[idx] = constructCustomField(itemType, piece, "") + } + return items + case customFieldFormatNumber: + num, err := strconv.ParseFloat(value, 64) //nolint:gomnd + if err != nil { + // Let Jira API handle data type error for now. + return value + } else { + return customFieldTypeNumber(num) + } + case customFieldFormatAny: + fallthrough + case customFieldFormatString: + return customFieldTypeString(value) + case customFieldFormatJson: + return customFieldTypeJson{Json: value} + default: + // An unknown type like "version" or "user". Try parsing as JSON, + // and if that doesn't work, just send as a string + var unused any + err := json.Unmarshal([]byte(value), &unused) + if err == nil { + return customFieldTypeJson{Json: value} + } else { + return value + } + } +} diff --git a/pkg/jira/edit.go b/pkg/jira/edit.go index 405951d2..9afd9625 100644 --- a/pkg/jira/edit.go +++ b/pkg/jira/edit.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "net/http" - "strconv" "strings" ) @@ -366,39 +365,21 @@ func constructCustomFieldsForEdit(fields map[string]string, configuredFields []I if identifier != strings.ToLower(key) { continue } - - switch configured.Schema.DataType { - case customFieldFormatOption: - data.Update.M.customFields[configured.Key] = []customFieldTypeOptionSet{{Set: customFieldTypeOption{Value: val}}} - case customFieldFormatProject: - data.Update.M.customFields[configured.Key] = []customFieldTypeProjectSet{{Set: customFieldTypeProject{Value: val}}} - case customFieldFormatArray: + if configured.Schema.DataType == customFieldFormatArray { pieces := strings.Split(strings.TrimSpace(val), ",") - if configured.Schema.Items == customFieldFormatOption { - items := make([]customFieldTypeOptionAddRemove, 0) - for _, p := range pieces { - if strings.HasPrefix(p, separatorMinus) { - items = append(items, customFieldTypeOptionAddRemove{Remove: &customFieldTypeOption{Value: strings.TrimPrefix(p, separatorMinus)}}) - } else { - items = append(items, customFieldTypeOptionAddRemove{Add: &customFieldTypeOption{Value: p}}) - } + items := make([]customFieldEditTypeAddRemove, len(pieces)) + for idx, piece := range pieces { + field := constructCustomField(configured.Schema.Items, "", strings.TrimPrefix(piece, separatorMinus)) + if strings.HasPrefix(piece, separatorMinus) { + items[idx] = customFieldEditTypeAddRemove{Remove: &field} + } else { + items[idx] = customFieldEditTypeAddRemove{Add: &field} } - data.Update.M.customFields[configured.Key] = items - } else { - data.Update.M.customFields[configured.Key] = pieces - } - case customFieldFormatNumber: - num, err := strconv.ParseFloat(val, 64) //nolint:gomnd - if err != nil { - // Let Jira API handle data type error for now. - data.Update.M.customFields[configured.Key] = []customFieldTypeStringSet{{Set: val}} - } else { - data.Update.M.customFields[configured.Key] = []customFieldTypeNumberSet{{Set: customFieldTypeNumber(num)}} } - case customFieldFormatJson: - data.Update.M.customFields[configured.Key] = []customFieldTypeJsonSet{{Set: customFieldTypeJson{Json: val}}} - default: - data.Update.M.customFields[configured.Key] = []customFieldTypeStringSet{{Set: val}} + data.Update.M.customFields[configured.Key] = items + } else { + field := constructCustomField(configured.Schema.DataType, "", val) + data.Update.M.customFields[configured.Key] = []customFieldEditTypeSet{{Set: field}} } } } From c51ba28bba94f6e216b7f56c0669429395fda4b4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 8 Sep 2024 15:54:52 +0100 Subject: [PATCH 5/5] Fixed bug when setting array type custom fields Co-authored-by: Ankit --- pkg/jira/customfield.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/jira/customfield.go b/pkg/jira/customfield.go index 0b299d6d..39fcfec3 100644 --- a/pkg/jira/customfield.go +++ b/pkg/jira/customfield.go @@ -57,7 +57,7 @@ func constructCustomField(dataType string, itemType string, value string) any { pieces := strings.Split(strings.TrimSpace(value), ",") items := make([]any, len(pieces)) for idx, piece := range pieces { - items[idx] = constructCustomField(itemType, piece, "") + items[idx] = constructCustomField(itemType, "", piece) } return items case customFieldFormatNumber: