Note

We've packed up and moved from Confluence to Discourse to bring you a better, more interactive space. Out of courtesy we didn't migrate your user account so - you will have to signup again

The Exalate team will be on holiday for the coming days - returning Jan 4
Enjoy & stay safe

Introduction

Jira Cloud comment supports formatting like bold, italic, underline , colour fonts and many more. To sync this to Zendesk is not totally supported by Exalate. The reason being, Jira cloud is using Mark Up language where Zendesk is using Markdown and HTML both. Exalate has default methods and scripts to convert into Markdown but that has some limitation like it will not support colour fonts , images and few more.


To achieve this , here is the advance script which helps you to create comment with html content in Zendesk.

Also i added how we can sync comments and description inline images from zendesk to Jira cloud   


The code:

Jira Cloud Outgoing Sync Script
//replica.comments = issue.comments
replica.comments = nodeHelper.getHtmlComments(issue)
replica.description    = nodeHelper.getHtmlField(issue,"description")

Zendesk Incoming Sync Script
issue.labels = replica.labels
issue.summary = replica.summary
def imgInDesc=false
def replicaDesc=replica.description
if(firstSync){
if(replicaDesc){
  if(replicaDesc.contains("img src")){
    imgInDesc=true
  }
    replicaDesc=replicaDesc.replaceAll("<ins>", "<u>").replaceAll("</ins>", "</u>")
    String htmlDesc=replicaDesc.replaceAll(/<img src="\/attachments\/(\d+)\?name=([^"]+)" alt="([^"]+)">/) { fullMatch, id, imageName, altText ->
  return ""
}
//debug.error(""+htmlDesc)
  issue.description= nodeHelper.toMarkDownFromHtml(htmlDesc)
}
else{
  issue.description= "No description"
}
}

issue.attachments = attachmentHelper.mergeAttachments(issue, replica)

  store(issue)
  
 def jsonSlurper = new groovy.json.JsonSlurper()
       def attachmentsMap = [:]      
 httpClient.http(
            "GET",
            "/api/v2/tickets/${issue.key}/comments",
            null,
            [:],
            ["Accept": ["application/json"], "Content-type": ["application/json"]])    
            {    
              req,response2->
              if(response2.code >= 400) {

                throw new com.exalate.api.exception.IssueTrackerException("1 Failed to get comments with status code  " + response2.code + " and with details " + response2.body)
              }
              else {
              def commentResponse = response2.body
              if(commentResponse?.comments?.size()>0)
              {
                commentResponse.comments.each { comment ->
                if (comment.attachments) { // Check if attachments exist
                comment.attachments.each { attachment ->
                attachmentsMap[attachment.file_name] = attachment.content_url
                  }
                  }
                } 
              }
              }
            }
//for all comments   
if(replicaDesc && imgInDesc)
  {
    def regex= /<img src="\/attachments\/(\d+)\?name=([^"]+)" alt="([^"]+)">/
    def matches = []
    def allMatches = replicaDesc.findAll(regex)
    allMatches.each { match ->
    // In this case, 'match' will be the captured `src` attribute
    matches << match
    }

    def imgMatches=[]
    imgMatches << "desc images:"
  matches.each { src ->
    def img1=src.replaceAll(/<img src="\/attachments\/(\d+)\?name=([^"]+)" alt="([^"]+)">/) { fullMatch, id, imageName, altText ->
    String newToken = attachmentsMap[imageName]
    if (newToken) {
        return """<img src="${newToken}" alt="${altText}" />"""
    } else {
        return fullMatch // return original if no mapping found
    }
  }
    imgMatches << img1
  }
def htmlExtraComment=imgMatches.join("\n")
//debug.error(imgMatches)
   def json = new groovy.json.JsonBuilder()
json.ticket {
    comment {
        html_body htmlExtraComment
    }
}
def jsonString = json.toString()
 httpClient.http("PUT","/api/v2/tickets/${issue.key}.json",jsonString,[:],["Accept": ["application/json"], "Content-type": ["application/json"]])
 {
req,response->

        if (response.code >= 400) {

          throw new com.exalate.api.exception.IssueTrackerException("Failed to create html comment with status code  " + response.code + " and with details " + response.body)
        }
 } 
   
  }         
replica.addedComments.each 
{
  it ->
    def commentBody = it.body
  //String jsonString = 
def json = new groovy.json.JsonBuilder()
commentBody = commentBody.replaceAll("<ins>", "<u>").replaceAll("</ins>", "</u>")
String updatedHtmlContent = commentBody.replaceAll(/<img src="\/attachments\/(\d+)\?name=([^"]+)" alt="([^"]+)">/) { fullMatch, id, imageName, altText ->
  //  debug.error(""+imageName)
    String newToken = attachmentsMap[imageName]
    if (newToken) {
        return """<img src="${newToken}" alt="${altText}" />"""
    } else {
        return fullMatch // return original if no mapping found
    }
}

//debug.error(""+updatedHtmlContent)

json.ticket {
    comment {
        html_body updatedHtmlContent
    }
}

def jsonString = json.toString()
 httpClient.http("PUT","/api/v2/tickets/${issue.key}.json",jsonString,[:],["Accept": ["application/json"], "Content-type": ["application/json"]])
 {
req,response->

        if (response.code >= 400) {

          throw new com.exalate.api.exception.IssueTrackerException("Failed to create html comment with status code  " + response.code + " and with details " + response.body)
        }
      else {
        def response3=httpClient.http("GET",
            "/api/v2/tickets/${issue.key}/comments",
            null,
            [:],
            ["Accept": ["application/json"], "Content-type": ["application/json"]]
          ){
            rer2,response3->
          if (response3.code >= 400) {

                throw new com.exalate.api.exception.IssueTrackerException("Failed to get comments with status code  " + response3.code + " and with details " + response3.body)
              }
            else {
            //store(issue)
              def commentResponse = response3.body

              // Assuming you want to find the latest comment by the highest ID
              if(commentResponse?.comments?.size()>0)
              {
              def latestComment = commentResponse.comments[commentResponse.comments.size()-1] 
              def commentId=latestComment.id?:null
             // debug.error(""+commentId)
              if (commentId) {
                def trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
                  .setType(com.exalate.api.domain.twintrace.TraceType.COMMENT)
                  .setToSynchronize(true)
                  .setLocalId(commentId as String)
                  .setRemoteId(it.remoteId as String)
                  .setAction(com.exalate.api.domain.twintrace.TraceAction.NONE)
                traces.add(trace)
                Thread.sleep(5000)
              //  if(it.remoteId=="10820")
                 log.error(""+trace+" \n all:"+traces)
              }
              }
              //    debug.error(""+comments)
            }
          }
          }

      }
}




return new scala.Tuple2(issueKey, scala.collection.JavaConverters.asScalaBuffer(traces))

Now, because we are adding traces manually , as soon as Exalate adds the traces , Zendesk fetches that last synced comment and sync back to Jira cloud  , to avoid this we will filter the author in outgoing sync script.

Note: This will not work in case we impersonate the Zendesk comment 

Zendesk Outgoing Sync Script
replica.description    = nodeHelper.getHtmlDescription(issue)
def allcomments=nodeHelper.getHtmlComments(issue)
def replicaComment=[]
allcomments.each{
  it->
  if(it.author.displayName != "Proxy User Name")
  {
    replicaComment << it
  }
}
replica.comments = replicaComment
Jira Cloud Incoming Sync Script
if (firstSync) {
    issue.projectKey  = "SOURCE"
    // Set the same issue type as the source issue. If not found, set a default.
    issue.typeName    = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "New Feature"
    issue.description  = nodeHelper.toMarkDownFromHtml(replica.description)

}
issue.summary      = replica.summary
issue.attachments  = attachmentHelper.mergeAttachments(issue, replica)
issue.labels       = replica.labels
issue.comments     = nodeHelper.toMarkDownComments(commentHelper.mergeComments(issue, replica))

Questions