Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add custom field clean method #62

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 66 additions & 36 deletions card.go
Expand Up @@ -82,7 +82,7 @@ type Card struct {
// Custom Fields
CustomFieldItems []*CustomFieldItem `json:"customFieldItems,omitempty"`

customFieldMap *map[string]interface{}
customFieldMap map[string]*CustomFieldValue
}

// CreatedAt returns the receiver card's created-at attribute as time.Time.
Expand All @@ -92,55 +92,85 @@ func (c *Card) CreatedAt() time.Time {
}

// CustomFields returns the card's custom fields.
func (c *Card) CustomFields(boardCustomFields []*CustomField) map[string]interface{} {
func (c *Card) CustomFields(boardCustomFields []*CustomField) map[string]*CustomFieldValue {

cfm := c.customFieldMap
// if there is no custom fields in card struct → return nil
if c.CustomFieldItems == nil {
return nil
}

if cfm == nil {
cfm = &(map[string]interface{}{})
// if customFieldMap already exist → return it
if c.customFieldMap != nil {
return c.customFieldMap
}

// bcfNames[CustomFieldItem ID] = Custom Field Name
bcfNames := map[string]string{}
// customFieldsMap[CustomFieldName] = CustomFieldValue
customFieldsMap := make(map[string]*CustomFieldValue)

// bcfOptionsMap[CustomField ID][ID of the option] = Value of the option
bcfOptionsMap := map[string]map[string]interface{}{}
// boardCustomFieldsNames[CustomFieldItem.IDCustomField] = CustomFieldName
boardCustomFieldsNames := make(map[string]string)

for _, bcf := range boardCustomFields {
bcfNames[bcf.ID] = bcf.Name
// boardCustomFieldsOptionsMap[CustomFieldItem.IDCustomField][CustomFieldOptionID] =
// CustomFieldOptionIDValue
boardCustomFieldsOptions := make(map[string]map[string]*CustomFieldValue)

//Options for Dropbox field
for _, cf := range bcf.Options {
// create 2nd level map when not available yet
map2, ok := bcfOptionsMap[cf.IDCustomField]
if !ok {
map2 = map[string]interface{}{}
bcfOptionsMap[bcf.ID] = map2
}
for _, boardCustomField := range boardCustomFields {
boardCustomFieldsNames[boardCustomField.ID] = boardCustomField.Name

bcfOptionsMap[bcf.ID][cf.ID] = cf.Value.Text
// collect options for the custom field with dropdown
for _, customField := range boardCustomField.Options {
// make map if there is no one
_, ok := boardCustomFieldsOptions[customField.IDCustomField]
if !ok {
boardCustomFieldsOptions[boardCustomField.ID] =
make(map[string]*CustomFieldValue)
}

boardCustomFieldsOptions[boardCustomField.ID][customField.ID] =
&CustomFieldValue{
Text: customField.Value.Text,
}
}
}

for _, customField := range c.CustomFieldItems {
// if there is a name in board custom fields names
if customFieldName, customFieldNameExist :=
boardCustomFieldsNames[customField.IDCustomField]; customFieldNameExist {

for _, cf := range c.CustomFieldItems {
if name, ok := bcfNames[cf.IDCustomField]; ok {
if cf.Value.Get() != nil {
(*cfm)[name] = cf.Value.Get()
} else { // Dropbox
// create 2nd level map when not available yet
map2, ok := bcfOptionsMap[cf.IDCustomField]
if !ok {
continue
}
value, ok := map2[cf.IDValue]

if ok {
(*cfm)[name] = value
}
// if value not nil that it is not a dropdown custom field
if customField.Value != nil {
customFieldsMap[customFieldName] = customField.Value
continue
}

// else try to find option name
if optionsForCustomField, optionExist :=
boardCustomFieldsOptions[customField.IDCustomField]; optionExist {
if optionName, optionNameExist :=
optionsForCustomField[customField.IDValue]; optionNameExist {
customFieldsMap[customFieldName] = optionName
}
}

}
}
return *cfm

return customFieldsMap

}

// RemoveIDCustomField removes a custom field by ID from card
func (c *Card) RemoveIDCustomField(customFieldID string, customFieldItem *CustomFieldItem) error {
path := fmt.Sprintf("cards/%s/customField/%s/item", c.ID, customFieldID)
return c.client.Put(
path,
Arguments{
"idValue": "",
"value": "",
},
customFieldItem,
)
}

// MoveToList moves a card to a list given by listID.
Expand Down
31 changes: 26 additions & 5 deletions card_test.go
Expand Up @@ -6,6 +6,7 @@
package trello

import (
"reflect"
"testing"
"time"
)
Expand Down Expand Up @@ -69,13 +70,33 @@ func TestCardsCustomFields(t *testing.T) {
}

vf1, ok := fields["Field1"]
if !ok || vf1 != "F1 1st opt" {
t.Errorf("Expected Field1 to be 'F1 1st opt' but it was %v", vf1)
expected1 := &CustomFieldValue{Text: "F1 1st opt"}
if !ok || !reflect.DeepEqual(vf1, expected1) {
t.Errorf("\nExpected Field1:\n%#v\nbut it was:\n%#v", vf1, expected1)
}

vf2, ok := fields["Field2"]
if !ok || vf2 != "F2 2nd opt" {
t.Errorf("Expected Field1 to be 'F2 2nd opt' but it was %v", vf2)
expected2 := &CustomFieldValue{Text: "F2 2nd opt"}
if !ok || !reflect.DeepEqual(vf2.Text, expected2.Text) {
t.Errorf("\nExpected Field2:\n%#v\nbut it was:\n%#v", vf2, expected2)
}

}

func TestRemoveIDCustomField(t *testing.T) {
card := testCard(t)
card.client.BaseURL = mockResponse("customFields", "custom-fields-remove.json").URL
customFieldItem := &CustomFieldItem{
Value: &CustomFieldValue{
Text: "Text that should be deleted",
},
}
err := card.RemoveIDCustomField("customFieldDummyID", customFieldItem)
if err != nil {
t.Fatal(err)
}
if customFieldItem.Value != nil {
t.Fatalf("Custom field value should be nil, but %+v", customFieldItem)
}
}

Expand Down Expand Up @@ -232,7 +253,7 @@ func TestAddURLAttachmentToCard(t *testing.T) {
c.client.BaseURL = mockResponse("cards", "url-attachments.json").URL
attachment := Attachment{
Name: "Test",
URL: "https://github.com/test",
URL: "https://github.com/test",
}
err := c.AddURLAttachment(&attachment)
if err != nil {
Expand Down
107 changes: 7 additions & 100 deletions custom-fields.go
@@ -1,120 +1,27 @@
package trello

import (
"database/sql/driver"
"encoding/json"
"fmt"
"strconv"
"time"
)

// CustomFieldItem represents the custom field items of Trello a trello card.
type CustomFieldItem struct {
ID string `json:"id,omitempty"`
Value CustomFieldValue `json:"value,omitempty"`
IDValue string `json:"idValue,omitempty"`
IDCustomField string `json:"idCustomField,omitempty"`
IDModel string `json:"idModel,omitempty"`
IDModelType string `json:"modelType,omitempty"`
ID string `json:"id,omitempty"`
Value *CustomFieldValue `json:"value,omitempty"`
IDValue string `json:"idValue,omitempty"`
IDCustomField string `json:"idCustomField,omitempty"`
IDModel string `json:"idModel,omitempty"`
IDModelType string `json:"modelType,omitempty"`
}

// CustomFieldValue represents the custom field value struct
// CustomFieldValue represents value of the custom field
type CustomFieldValue struct {
val interface{}
}

type cfval struct {
Text string `json:"text,omitempty"`
Number string `json:"number,omitempty"`
Date string `json:"date,omitempty"`
Checked string `json:"checked,omitempty"`
}

// NewCustomFieldValue the custom field constructor
func NewCustomFieldValue(val interface{}) CustomFieldValue {
return CustomFieldValue{val: val}
}

const timeFmt = "2006-01-02T15:04:05Z"

// Get the custom field value getter
func (v CustomFieldValue) Get() interface{} {
return v.val
}

// String the custom field String method
func (v CustomFieldValue) String() string {
return fmt.Sprintf("%s", v.val)
}

// MarshalJSON the custom field marchaller
func (v CustomFieldValue) MarshalJSON() ([]byte, error) {
val := v.val

switchVal:
switch v := val.(type) {
case driver.Valuer:
var err error
val, err = v.Value()
if err != nil {
return nil, err
}
goto switchVal
case string:
return json.Marshal(cfval{Text: v})
case int, int64:
return json.Marshal(cfval{Number: fmt.Sprintf("%d", v)})
case float64:
return json.Marshal(cfval{Number: fmt.Sprintf("%f", v)})
case bool:
if v {
return json.Marshal(cfval{Checked: "true"})
}
return json.Marshal(cfval{Checked: "false"})
case time.Time:
return json.Marshal(cfval{Date: v.Format(timeFmt)})
default:
return nil, fmt.Errorf("unsupported type")
}
}

// UnmarshalJSON the custom field umarshaller
func (v *CustomFieldValue) UnmarshalJSON(b []byte) error {
cfval := cfval{}
err := json.Unmarshal(b, &cfval)
if err != nil {
return err
}
if cfval.Text != "" {
v.val = cfval.Text
}
if cfval.Date != "" {
v.val, err = time.Parse(timeFmt, cfval.Date)
if err != nil {
return err
}
}
if cfval.Checked != "" {
v.val = cfval.Checked == "true"
}
if cfval.Number != "" {
v.val, err = strconv.Atoi(cfval.Number)
if err != nil {
v.val, err = strconv.ParseFloat(cfval.Number, 64)
if err != nil {
v.val, err = strconv.ParseFloat(cfval.Number, 32)
if err != nil {
v.val, err = strconv.ParseInt(cfval.Number, 10, 64)
if err != nil {
return fmt.Errorf("cannot convert %s to number", cfval.Number)
}
}
}
}
}
return nil
}

// CustomField represents Trello's custom fields: "extra bits of structured data
// attached to cards when our users need a bit more than what Trello provides."
// https://developers.trello.com/reference/#custom-fields
Expand Down
7 changes: 7 additions & 0 deletions testdata/customFields/custom-fields-remove.json
@@ -0,0 +1,7 @@
{
"id": "dummyID",
"value": null,
"idCustomField": "customFieldDummyID",
"idModel": "dummyIDModel",
"modelType": "card"
}