From 576c9f6b534cb648fe879e2247eb7ae60eabfa3d Mon Sep 17 00:00:00 2001 From: Hitesh Pattanayak <48874082+HiteshRepo@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:05:33 +0530 Subject: [PATCH] handles single & multiple values for metadata columns (#5121) handles single & multiple values for metadata columns Similar to `Hyperlink` and `Column` columns, `Metadata` column too is unrecognizable from GRAPH API response. Hence identifying from the field column names. `Metadata` fields are like tags. A `Metadata` fields can be configured to hold multiple values/tags **Original List with `Metadata` column (Department) with single value/tag**: ![Metadata-List](https://github.com/alcionai/corso/assets/48874082/0b913a2a-46d5-4d9c-83f9-69a5236b1024) **Restored List with `Metadata` column with single value/tag**: ![Restored-Metadata-List](https://github.com/alcionai/corso/assets/48874082/9420012b-345c-4fac-90c3-c0d421b2edfb) **Original List with `Metadata` column (Department) with multiple value/tag**: ![Metadata-List-Multi](https://github.com/alcionai/corso/assets/48874082/054ef4a1-c46e-48ba-b410-a95b540cde33) **Restored List with `Metadata` column with multiple value/tag**: ![Restored-Multi-Metadata-List](https://github.com/alcionai/corso/assets/48874082/ef6c904b-e431-4a85-9ef2-f08bcf8e21e4) #### Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No #### Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Supportability/Tests - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup #### Issue(s) #5084 #5108 #### Test Plan - [x] :muscle: Manual - [x] :zap: Unit test - [x] :green_heart: E2E --- src/pkg/services/m365/api/consts.go | 4 + src/pkg/services/m365/api/lists.go | 59 +++++++++ src/pkg/services/m365/api/lists_test.go | 155 ++++++++++++++++++++++++ 3 files changed, 218 insertions(+) diff --git a/src/pkg/services/m365/api/consts.go b/src/pkg/services/m365/api/consts.go index b5342f8c80..e9edb623ae 100644 --- a/src/pkg/services/m365/api/consts.go +++ b/src/pkg/services/m365/api/consts.go @@ -62,6 +62,10 @@ const ( PersonEmailKey = "Email" + MetadataLabelKey = "Label" + MetadataTermGUIDKey = "TermGuid" + MetadataWssIDKey = "WssId" + LinkTitleFieldNamePart = "LinkTitle" ChildCountFieldNamePart = "ChildCount" LookupIDFieldNamePart = "LookupId" diff --git a/src/pkg/services/m365/api/lists.go b/src/pkg/services/m365/api/lists.go index 4a5e76f91d..5c5ec2d1b1 100644 --- a/src/pkg/services/m365/api/lists.go +++ b/src/pkg/services/m365/api/lists.go @@ -511,6 +511,10 @@ func retrieveFieldData(orig models.FieldValueSetable, columnNames map[string]*co additionalData[fieldName] = concatenatedHyperlink } + if metadataField, fieldName, ok := hasMetadataFields(additionalData); ok { + additionalData[fieldName] = concatenateMetadataFields(metadataField) + } + fields.SetAdditionalData(additionalData) return fields @@ -647,6 +651,44 @@ func hasHyperLinkFields(additionalData map[string]any) (map[string]any, string, return nil, "", false } +func hasMetadataFields(additionalData map[string]any) ([]map[string]any, string, bool) { + for fieldName, value := range additionalData { + switch valType := reflect.TypeOf(value).Kind(); valType { + case reflect.Map: + metadataFields, areMetadataFields := getMetadataFields(value) + if areMetadataFields { + return []map[string]any{metadataFields}, fieldName, true + } + + case reflect.Slice: + mmdfs := make([]map[string]any, 0) + + multiMetadataFields, ok := value.([]any) + if !ok { + continue + } + + for _, mdfs := range multiMetadataFields { + metadataFields, areMetadataFields := getMetadataFields(mdfs) + if areMetadataFields { + mmdfs = append(mmdfs, metadataFields) + } + } + + if len(mmdfs) > 0 { + return mmdfs, fieldName, true + } + } + } + + return nil, "", false +} + +func getMetadataFields(metadataFieldvalue any) (map[string]any, bool) { + nestedFields, ok := metadataFieldvalue.(map[string]any) + return nestedFields, ok && keys.HasKeys(nestedFields, MetadataLabelKey, MetadataTermGUIDKey, MetadataWssIDKey) +} + func concatenateAddressFields(addressFields map[string]any) string { parts := make([]string, 0) @@ -692,6 +734,23 @@ func concatenateHyperLinkFields(hyperlinkFields map[string]any) string { return "" } +func concatenateMetadataFields(metadataFieldsArr []map[string]any) string { + labels := make([]string, 0) + + for _, md := range metadataFieldsArr { + mdVal, ok := md[MetadataLabelKey].(*string) + if ok { + labels = append(labels, ptr.Val(mdVal)) + } + } + + if len(labels) > 0 { + return strings.Join(labels, ",") + } + + return "" +} + func addressKeyToVal(fields map[string]any, key string) string { if v, err := str.AnyValueToString(key, fields); err == nil { return v diff --git a/src/pkg/services/m365/api/lists_test.go b/src/pkg/services/m365/api/lists_test.go index d0d08f396a..1417d84153 100644 --- a/src/pkg/services/m365/api/lists_test.go +++ b/src/pkg/services/m365/api/lists_test.go @@ -941,6 +941,161 @@ func (suite *ListsUnitSuite) TestSetAdditionalDataByColumnNames() { } } +func (suite *ListsUnitSuite) TestHasMetadataFields() { + t := suite.T() + + tests := []struct { + name string + additionalData map[string]any + expectedFields []map[string]any + expectedFieldName string + hasMetadataFields bool + }{ + { + name: "Single metadata fields, has all keys", + additionalData: map[string]any{ + "MdCol": map[string]any{ + MetadataLabelKey: ptr.To("Engineering"), + MetadataTermGUIDKey: ptr.To("6b5d3ce9-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(4), + }, + }, + expectedFields: []map[string]any{ + { + MetadataLabelKey: ptr.To("Engineering"), + MetadataTermGUIDKey: ptr.To("6b5d3ce9-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(4), + }, + }, + expectedFieldName: "MdCol", + hasMetadataFields: true, + }, + { + name: "Multiple metadata fields, has all keys", + additionalData: map[string]any{ + "MdCol": []any{ + map[string]any{ + MetadataLabelKey: ptr.To("Engineering"), + MetadataTermGUIDKey: ptr.To("6b5d3ce9-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(4), + }, + map[string]any{ + MetadataLabelKey: ptr.To("Marketing"), + MetadataTermGUIDKey: ptr.To("312347ce-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(2), + }, + }, + }, + expectedFields: []map[string]any{ + { + MetadataLabelKey: ptr.To("Engineering"), + MetadataTermGUIDKey: ptr.To("6b5d3ce9-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(4), + }, + { + MetadataLabelKey: ptr.To("Marketing"), + MetadataTermGUIDKey: ptr.To("312347ce-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(2), + }, + }, + expectedFieldName: "MdCol", + hasMetadataFields: true, + }, + { + name: "Single metadata fields, missing few keys", + additionalData: map[string]any{ + "MdCol": map[string]any{ + MetadataLabelKey: ptr.To("Engineering"), + }, + }, + expectedFields: nil, + expectedFieldName: "", + hasMetadataFields: false, + }, + { + name: "Multiple metadata fields, missing few keys", + additionalData: map[string]any{ + "MdCol": []any{ + map[string]any{ + MetadataLabelKey: ptr.To("Engineering"), + MetadataTermGUIDKey: ptr.To("6b5d3ce9-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(4), + }, + map[string]any{ + MetadataLabelKey: ptr.To("Marketing"), + }, + }, + }, + expectedFields: []map[string]any{ + { + MetadataLabelKey: ptr.To("Engineering"), + MetadataTermGUIDKey: ptr.To("6b5d3ce9-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(4), + }, + }, + expectedFieldName: "MdCol", + hasMetadataFields: true, + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + nestedFields, fName, isMetadata := hasMetadataFields(test.additionalData) + assert.Equal(t, test.expectedFields, nestedFields) + assert.Equal(t, test.expectedFieldName, fName) + assert.Equal(t, test.hasMetadataFields, isMetadata) + }) + } +} + +func (suite *ListsUnitSuite) TestConcatenateMetadataFields() { + t := suite.T() + + tests := []struct { + name string + metadataFields []map[string]any + expectedFieldName string + expectedResult string + hasMetadataFields bool + columnNames map[string]any + }{ + { + name: "Single metadata fields", + metadataFields: []map[string]any{ + { + MetadataLabelKey: ptr.To("Engineering"), + MetadataTermGUIDKey: ptr.To("6b5d3ce9-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(4), + }, + }, + expectedResult: "Engineering", + }, + { + name: "Multiple metadata fields", + metadataFields: []map[string]any{ + { + MetadataLabelKey: ptr.To("Engineering"), + MetadataTermGUIDKey: ptr.To("6b5d3ce9-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(4), + }, + { + MetadataLabelKey: ptr.To("Marketing"), + MetadataTermGUIDKey: ptr.To("312347ce-3043-499f-8be6-e92fb57bed96"), + MetadataWssIDKey: ptr.To(2), + }, + }, + expectedResult: "Engineering,Marketing", + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + res := concatenateMetadataFields(test.metadataFields) + assert.Equal(t, test.expectedResult, res) + }) + } +} + func (suite *ListsUnitSuite) TestCheckFields() { t := suite.T()