diff --git a/components/resources/work_packages/details.go b/components/resources/work_packages/details.go index 600c361..1d7342c 100644 --- a/components/resources/work_packages/details.go +++ b/components/resources/work_packages/details.go @@ -82,7 +82,7 @@ func workPackageDetails(dto *dtos.WorkPackageDto, schema *Schema) models.WorkPac Description: longTextRaw(dto.Description), ParentID: linkID(dto.Links, func(links *dtos.WorkPackageLinksDto) *dtos.LinkDto { return links.Parent }), Project: project, - Fields: dto.CustomFields, + Fields: normalizeCustomFields(dto.CustomFields, schema), FieldLabels: schema.fieldLabels(), } } @@ -131,3 +131,42 @@ func linkID(links *dtos.WorkPackageLinksDto, selector func(*dtos.WorkPackageLink id := parser.IdFromLink(link.Href) return &id } + +func normalizeCustomFields(fields map[string]any, schema *Schema) map[string]any { + if len(fields) == 0 { + return fields + } + + typesByAPIName := make(map[string]string, len(schema.Fields)) + for _, field := range schema.Fields { + typesByAPIName[field.APIName] = field.Type + } + + normalized := make(map[string]any, len(fields)) + for apiName, value := range fields { + if typesByAPIName[apiName] == "Formattable" { + if rawValue, ok := rawLongTextValue(value); ok { + normalized[apiName] = rawValue + continue + } + } + + normalized[apiName] = value + } + + return normalized +} + +func rawLongTextValue(value any) (string, bool) { + object, ok := value.(map[string]any) + if !ok { + return "", false + } + + raw, ok := object["raw"].(string) + if !ok { + return "", false + } + + return raw, true +} diff --git a/components/resources/work_packages/details_test.go b/components/resources/work_packages/details_test.go index 7c1b5a0..68e9231 100644 --- a/components/resources/work_packages/details_test.go +++ b/components/resources/work_packages/details_test.go @@ -330,3 +330,186 @@ func TestInspectPreservesDuplicateFieldLabels(t *testing.T) { t.Fatalf("expected duplicate KPI mappings, got %#v", payload.WorkPackage.FieldLabels) } } + +func TestInspectNormalizesFormattableCustomFieldsToRawStrings(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v3/work_packages/74172": + _, _ = io.WriteString(w, `{ + "id": 74172, + "subject": "Epic", + "customField401": { + "format": "markdown", + "html": "
Body
", + "raw": "Body" + }, + "_links": { + "self": {"href": "/api/v3/work_packages/74172"}, + "project": {"href": "/api/v3/projects/1482", "title": "CLI"}, + "schema": {"href": "/api/v3/work_packages/schemas/1482-6"}, + "status": {"href": "/api/v3/statuses/1", "title": "new"}, + "type": {"href": "/api/v3/types/6", "title": "Epic"}, + "assignee": {"href": null, "title": ""} + } + }`) + case "/api/v3/work_packages/schemas/1482-6": + _, _ = io.WriteString(w, `{ + "customField401": {"name": "Acceptance criteria", "type": "Formattable", "writable": true} + }`) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer server.Close() + + host, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + + requests.Init(host, "token", false) + + payload, err := work_packages.Inspect("74172") + if err != nil { + t.Fatal(err) + } + + if payload.WorkPackage.Fields["customField401"] != "Body" { + t.Fatalf("expected Formattable field raw string, got %#v", payload.WorkPackage.Fields["customField401"]) + } +} + +func TestInspectPassesThroughNonFormattableDictCustomField(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v3/work_packages/74173": + _, _ = io.WriteString(w, `{ + "id": 74173, + "subject": "Epic", + "customField402": { + "id": 9, + "name": "v1.0" + }, + "_links": { + "self": {"href": "/api/v3/work_packages/74173"}, + "project": {"href": "/api/v3/projects/1482", "title": "CLI"}, + "schema": {"href": "/api/v3/work_packages/schemas/1482-6"}, + "status": {"href": "/api/v3/statuses/1", "title": "new"}, + "type": {"href": "/api/v3/types/6", "title": "Epic"}, + "assignee": {"href": null, "title": ""} + } + }`) + case "/api/v3/work_packages/schemas/1482-6": + _, _ = io.WriteString(w, `{ + "customField402": {"name": "Release", "type": "Version", "writable": true} + }`) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer server.Close() + + host, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + + requests.Init(host, "token", false) + + payload, err := work_packages.Inspect("74173") + if err != nil { + t.Fatal(err) + } + + value, ok := payload.WorkPackage.Fields["customField402"].(map[string]any) + if !ok || value["name"] != "v1.0" { + t.Fatalf("expected non-Formattable dict value untouched, got %#v", payload.WorkPackage.Fields["customField402"]) + } +} + +func TestInspectPassesThroughFormattableFieldAlreadyAString(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v3/work_packages/74174": + _, _ = io.WriteString(w, `{ + "id": 74174, + "subject": "Epic", + "customField401": "Already raw", + "_links": { + "self": {"href": "/api/v3/work_packages/74174"}, + "project": {"href": "/api/v3/projects/1482", "title": "CLI"}, + "schema": {"href": "/api/v3/work_packages/schemas/1482-6"}, + "status": {"href": "/api/v3/statuses/1", "title": "new"}, + "type": {"href": "/api/v3/types/6", "title": "Epic"}, + "assignee": {"href": null, "title": ""} + } + }`) + case "/api/v3/work_packages/schemas/1482-6": + _, _ = io.WriteString(w, `{ + "customField401": {"name": "Acceptance criteria", "type": "Formattable", "writable": true} + }`) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer server.Close() + + host, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + + requests.Init(host, "token", false) + + payload, err := work_packages.Inspect("74174") + if err != nil { + t.Fatal(err) + } + + if payload.WorkPackage.Fields["customField401"] != "Already raw" { + t.Fatalf("expected plain string Formattable field untouched, got %#v", payload.WorkPackage.Fields["customField401"]) + } +} + +func TestInspectHandlesEmptyCustomFieldsMap(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v3/work_packages/74175": + _, _ = io.WriteString(w, `{ + "id": 74175, + "subject": "Epic", + "_links": { + "self": {"href": "/api/v3/work_packages/74175"}, + "project": {"href": "/api/v3/projects/1482", "title": "CLI"}, + "schema": {"href": "/api/v3/work_packages/schemas/1482-6"}, + "status": {"href": "/api/v3/statuses/1", "title": "new"}, + "type": {"href": "/api/v3/types/6", "title": "Epic"}, + "assignee": {"href": null, "title": ""} + } + }`) + case "/api/v3/work_packages/schemas/1482-6": + _, _ = io.WriteString(w, `{ + "customField401": {"name": "Acceptance criteria", "type": "Formattable", "writable": true} + }`) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer server.Close() + + host, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + + requests.Init(host, "token", false) + + payload, err := work_packages.Inspect("74175") + if err != nil { + t.Fatal(err) + } + + if len(payload.WorkPackage.Fields) != 0 { + t.Fatalf("expected empty fields map, got %#v", payload.WorkPackage.Fields) + } +}