@@ -19,13 +19,142 @@ package apiv2models
1919
2020import (
2121 "encoding/json"
22+ "strings"
2223 "time"
2324
2425 "github.com/apache/incubator-devlake/core/errors"
2526 "github.com/apache/incubator-devlake/core/models/common"
2627 "github.com/apache/incubator-devlake/plugins/jira/models"
2728)
2829
30+ // FlexibleDescription supports both plain text and ADF (Atlassian Document Format) for Jira description field
31+ // ADF reference: https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
32+ type FlexibleDescription struct {
33+ Value string
34+ }
35+
36+ // ADFNode represents a node in Atlassian Document Format
37+ type ADFNode struct {
38+ Type string `json:"type"`
39+ Text string `json:"text,omitempty"`
40+ Content []ADFNode `json:"content,omitempty"`
41+ Attrs map [string ]interface {} `json:"attrs,omitempty"`
42+ }
43+
44+ // UnmarshalJSON implements custom JSON unmarshaling for FlexibleDescription
45+ func (fd * FlexibleDescription ) UnmarshalJSON (data []byte ) error {
46+ // Handle null values
47+ if string (data ) == "null" {
48+ fd .Value = ""
49+ return nil
50+ }
51+
52+ // Try to unmarshal as string first
53+ var str string
54+ if err := json .Unmarshal (data , & str ); err == nil {
55+ fd .Value = str
56+ return nil
57+ }
58+
59+ // Try to unmarshal as ADF document
60+ var adfDoc ADFNode
61+ if err := json .Unmarshal (data , & adfDoc ); err == nil {
62+ fd .Value = extractTextFromADF (adfDoc )
63+ return nil
64+ }
65+
66+ // Fallback: keep raw JSON as string for debugging
67+ fd .Value = string (data )
68+ return nil
69+ }
70+
71+ // extractTextFromADF recursively extracts plain text from ADF document
72+ func extractTextFromADF (node ADFNode ) string {
73+ var result strings.Builder
74+
75+ switch node .Type {
76+ case "text" :
77+ result .WriteString (node .Text )
78+ case "hardBreak" :
79+ result .WriteString ("\n " )
80+ case "paragraph" :
81+ for _ , child := range node .Content {
82+ result .WriteString (extractTextFromADF (child ))
83+ }
84+ result .WriteString ("\n " )
85+ case "heading" :
86+ for _ , child := range node .Content {
87+ result .WriteString (extractTextFromADF (child ))
88+ }
89+ result .WriteString ("\n " )
90+ case "listItem" :
91+ for _ , child := range node .Content {
92+ result .WriteString (extractTextFromADF (child ))
93+ }
94+ case "bulletList" , "orderedList" :
95+ for _ , child := range node .Content {
96+ result .WriteString ("• " )
97+ result .WriteString (extractTextFromADF (child ))
98+ result .WriteString ("\n " )
99+ }
100+ case "table" :
101+ for _ , row := range node .Content {
102+ if row .Type == "tableRow" {
103+ for j , cell := range row .Content {
104+ if j > 0 {
105+ result .WriteString (" | " )
106+ }
107+ result .WriteString (extractTextFromADF (cell ))
108+ }
109+ result .WriteString ("\n " )
110+ }
111+ }
112+ case "tableCell" , "tableHeader" :
113+ for _ , child := range node .Content {
114+ result .WriteString (extractTextFromADF (child ))
115+ }
116+ case "codeBlock" :
117+ result .WriteString ("```\n " )
118+ for _ , child := range node .Content {
119+ result .WriteString (extractTextFromADF (child ))
120+ }
121+ result .WriteString ("\n ```\n " )
122+ case "blockquote" :
123+ result .WriteString ("> " )
124+ for _ , child := range node .Content {
125+ result .WriteString (extractTextFromADF (child ))
126+ }
127+ result .WriteString ("\n " )
128+ case "doc" :
129+ for _ , child := range node .Content {
130+ result .WriteString (extractTextFromADF (child ))
131+ }
132+ case "inlineCard" , "mention" :
133+ // Extract text from attrs or content for links and mentions
134+ if attrs , ok := node .Attrs ["text" ]; ok {
135+ if text , ok := attrs .(string ); ok {
136+ result .WriteString (text )
137+ }
138+ } else {
139+ for _ , child := range node .Content {
140+ result .WriteString (extractTextFromADF (child ))
141+ }
142+ }
143+ default :
144+ // For unknown types, extract content recursively
145+ for _ , child := range node .Content {
146+ result .WriteString (extractTextFromADF (child ))
147+ }
148+ }
149+
150+ return result .String ()
151+ }
152+
153+ // String returns the string value
154+ func (fd FlexibleDescription ) String () string {
155+ return fd .Value
156+ }
157+
29158type Issue struct {
30159 Expand string `json:"expand"`
31160 ID uint64 `json:"id,string"`
@@ -121,8 +250,8 @@ type Issue struct {
121250 ID string `json:"id"`
122251 Name string `json:"name"`
123252 } `json:"components"`
124- Timeoriginalestimate * int64 `json:"timeoriginalestimate"`
125- Description string `json:"description"`
253+ Timeoriginalestimate * int64 `json:"timeoriginalestimate"`
254+ Description FlexibleDescription `json:"description"`
126255 Timetracking * struct {
127256 RemainingEstimate string `json:"remainingEstimate"`
128257 TimeSpent string `json:"timeSpent"`
@@ -233,7 +362,7 @@ func (i Issue) toToolLayer(connectionId uint64) *models.JiraIssue {
233362 IssueKey : i .Key ,
234363 StoryPoint : & workload ,
235364 Summary : i .Fields .Summary ,
236- Description : i .Fields .Description ,
365+ Description : i .Fields .Description . Value ,
237366 Type : i .Fields .Issuetype .ID ,
238367 StatusName : i .Fields .Status .Name ,
239368 StatusKey : i .Fields .Status .StatusCategory .Key ,
0 commit comments