11import fetch from "node-fetch" ;
22import { Importer , ImportResult , IssuePriority } from "../../types" ;
3+ import type { Comment as LinearComment } from "../../types" ;
34
45type ClickupPriority = "urgent" | "high" | "normal" | "low" | null ;
56
@@ -40,6 +41,21 @@ interface ClickupTask {
4041 due_date ?: string ;
4142}
4243
44+ interface ClickupComment {
45+ id : string ;
46+ comment_text ?: string ;
47+ date ?: string ;
48+ user ?: {
49+ id ?: number ;
50+ username ?: string ;
51+ email ?: string ;
52+ } ;
53+ }
54+
55+ interface ClickupCommentsResponse {
56+ comments : ClickupComment [ ] ;
57+ }
58+
4359interface ClickupTaskResponse {
4460 tasks : ClickupTask [ ] ;
4561}
@@ -52,7 +68,7 @@ export interface ClickupImporterOptions {
5268 statusMapping ?: Record < string , string > ;
5369 maxIssues ?: number ;
5470 singleTaskId ?: string ;
55- fetchImpl ?: typeof fetch ;
71+ fetchImpl ?: ( url : string , init ?: any ) => Promise < any > ;
5672}
5773
5874/**
@@ -67,7 +83,7 @@ export class ClickupApiImporter implements Importer {
6783 this . statusMapping = options . statusMapping ?? { } ;
6884 this . maxIssues = options . maxIssues ?? 1 ;
6985 this . singleTaskId = options . singleTaskId ;
70- this . fetchImpl = options . fetchImpl ?? fetch ;
86+ this . fetchImpl = ( options . fetchImpl ?? fetch ) as ( url : string , init ?: any ) => Promise < any > ;
7187 }
7288
7389 public get name ( ) : string {
@@ -90,12 +106,17 @@ export class ClickupApiImporter implements Importer {
90106 for ( const task of tasks ) {
91107 const baseDescription = task . description || task . text_content || undefined ;
92108 const originalUrl = task . url ;
109+ const comments = await this . fetchComments ( task . id ) ;
110+ const commentsBlock = this . formatComments ( comments ) ;
111+
93112 const description =
94113 originalUrl && baseDescription
95114 ? `${ baseDescription } \n\n[View original task in ClickUp](${ originalUrl } )`
96115 : originalUrl
97116 ? `[View original task in ClickUp](${ originalUrl } )`
98117 : baseDescription ;
118+ const fullDescription =
119+ commentsBlock && description ? `${ description } \n\n---\n\n${ commentsBlock } ` : commentsBlock || description ;
99120 const statusName = this . mapStatus ( task . status ?. status ) ;
100121 const priority = this . mapPriority ( task . priority ?. priority ) ;
101122
@@ -133,7 +154,7 @@ export class ClickupApiImporter implements Importer {
133154
134155 importData . issues . push ( {
135156 title : task . name ,
136- description,
157+ description : fullDescription ,
137158 status : statusName ,
138159 priority,
139160 url : originalUrl ,
@@ -224,12 +245,55 @@ export class ClickupApiImporter implements Importer {
224245 return Number . isNaN ( num ) ? undefined : new Date ( num ) ;
225246 }
226247
248+ private async fetchComments ( taskId : string ) : Promise < LinearComment [ ] > {
249+ try {
250+ const response = await this . fetchImpl ( `${ this . apiBaseUrl } /task/${ taskId } /comment` , {
251+ headers : {
252+ Authorization : this . apiToken ,
253+ "Content-Type" : "application/json" ,
254+ } ,
255+ } ) ;
256+
257+ if ( ! response . ok ) {
258+ return [ ] ;
259+ }
260+
261+ const data = ( await response . json ( ) ) as ClickupCommentsResponse ;
262+ return ( data . comments || [ ] ) . map ( comment => {
263+ const userKey = ( comment . user ?. email || comment . user ?. username || "" ) . toLowerCase ( ) ;
264+ return {
265+ body : comment . comment_text ,
266+ userId : userKey || "unknown" ,
267+ createdAt : this . toDate ( comment . date ) ,
268+ } ;
269+ } ) ;
270+ } catch {
271+ return [ ] ;
272+ }
273+ }
274+
275+ private formatComments ( comments : LinearComment [ ] ) : string | undefined {
276+ if ( ! comments . length ) {
277+ return undefined ;
278+ }
279+ const sorted = [ ...comments ] . sort ( ( a , b ) => {
280+ const aTime = a . createdAt ? a . createdAt . getTime ( ) : 0 ;
281+ const bTime = b . createdAt ? b . createdAt . getTime ( ) : 0 ;
282+ return aTime - bTime ;
283+ } ) ;
284+ const blocks = sorted . map ( comment => {
285+ const date = comment . createdAt ? comment . createdAt . toISOString ( ) . split ( "T" ) [ 0 ] : "" ;
286+ return `**${ comment . userId || "Unknown" } ** ${ date } \n\n${ comment . body ?? "" } ` ;
287+ } ) ;
288+ return `### ClickUp comments\n\n${ blocks . join ( "\n\n---\n\n" ) } ` ;
289+ }
290+
227291 private listId : string ;
228292 private apiToken : string ;
229293 private apiBaseUrl : string ;
230294 private labelForBoard ?: string ;
231295 private statusMapping : Record < string , string > ;
232296 private maxIssues : number ;
233297 private singleTaskId ?: string ;
234- private fetchImpl : typeof fetch ;
298+ private fetchImpl : ( url : string , init ?: any ) => Promise < any > ;
235299}
0 commit comments