1
0
-1

When syncing description or any wiki field from Azure to Jira on-Prem, images are not getting copied properly. Its shows thumbnail of the image instead of actual image like below:


Is there any reason for same? Is there any way to fix the same.

Also when syncing from Jira to Azure it gives below line:


I am using the transform scripts for the syncing and both scripts are having some loopholes it seems.


Can anyone help?

    CommentAdd your comment...

    4 answers

    1.  
      2
      1
      0

      Hi 



      Update as of 13 July.


      Outgoing sync Jira On Prem:



      import com.atlassian.jira.component.ComponentAccessor
      
      class WikiToHtml {
      	static String transform(String wikiFormat) {
      		if (!wikiFormat) {
      			return null
      		}
      
      		// access the correct services
      		def jcl = ComponentAccessor.classLoader
      		def app = ComponentAccessor.getApplicationProperties()
      		def epubClass = jcl.loadClass("com.atlassian.event.api.EventPublisher")
      		def epub = ComponentAccessor.getOSGiComponentInstanceOfType(epubClass)
      		def fmanClass = jcl.loadClass("com.atlassian.jira.config.FeatureManager")
      		def fman = ComponentAccessor.getOSGiComponentInstanceOfType(fmanClass)
      		def vreqClass = jcl.loadClass("com.atlassian.jira.util.velocity.VelocityRequestContextFactory")
      		def vreq = ComponentAccessor.getOSGiComponentInstanceOfType(vreqClass)
      		def wrenderClass = jcl.loadClass("com.atlassian.jira.issue.fields.renderer.wiki.AtlassianWikiRenderer")
      		def wrender = wrenderClass.newInstance(epub, app, vreq, fman)
      
      
      		def fixImage = wikiFormat?.replaceAll(/\!(\S+)\|\S+\!/, '<!-- inline image filename=#$1# -->')
      		fixImage = fixImage.replaceAll(/\!\^(\S+)\|\S+\!/, '<!-- inline image filename=#$1# -->')
      		fixImage = fixImage.replaceAll(/\!\^(\S+)\!/, '<!-- inline image filename=#$1# -->')
      		fixImage = fixImage.replaceAll(/\!(\S+)\!/, '<!-- inline image filename=#$1# -->')
      
      		// wiki text can also contain files
      		fixImage = fixImage.replaceAll(/\[(\S+)\|\^(\S+)\]/, '<!-- inline file filename=#$2# -->')
      		fixImage = fixImage.replaceAll(/\[\^(\S+)\]/, '<!-- inline file filename=#$1# -->')
      		return wrender.render(fixImage, null)
      
      	}
      
      }
      
      replica.description = WikiToHtml.transform(issue.description)
      replica.labels         = issue.labels
      replica.comments       = issue.comments.collect {
          comment -> 
          comment.body = WikiToHtml.transform (comment.body)
          comment
      }







      Incoming Sync Azure Devops:


      if(firstSync){
         // Set type name from source entity, if not found set a default
         workItem.projectKey  =  "Mathieu"
         workItem.typeName = nodeHelper.getIssueType(replica.type?.name)?.name ?: "Task";
      }
      
      workItem.summary  = replica.summary
        
      workItem.attachments  = attachmentHelper.mergeAttachments(workItem, replica)
       
       
      def processInlineImages = { str ->
          def processUnescapedLtGtTags = {
              def counter = 0
              while (counter < 1000) {
                  def matcher = (str =~ /<!-- inline image filename=#(([^#]+)|(([^#]+)#([^#]+)))# -->/)
                  if (matcher.size() < 1) {
                      break;
                  }
                  def match = matcher[0]
                  if (match.size() < 2) {
                      break;
                  }
                  //log.error("replica.attachments=${replica.attachments}")
                  def attId = replica.attachments.find { it.filename?.equals(match[1]) }?.remoteId
                  if (!attId) {
                      log.error("""Could not find attachment with name ${match[1]}, 
                 known names: ${replica.attachments.filename}, 
                 match: ${replica.attachments.find { it.filename?.equals(match[1]) }}
             """)
                      str = str.replace(match[0], """<!-- inline processed image filename=#${match[1]}# -->""".toString())
                  } else {
                      def tmpStr = str.replace(match[0], """<img src="/secure/attachment/${attId}/${attId}_${match[1]}" />""".toString())
                      if (tmpStr == str) {
                          break;
                      }
                      str = tmpStr
                  }
                  counter++
              }
              str
          }
          def processLtGtTags = {
              def counter = 0
              while (counter < 1000) {
                  def matcher = (str =~ /<!-- inline image filename=#(([^#]+)|(([^#]+)#([^#]+)))# -->/)
                  if (matcher.size() < 1) {
                      break;
                  }
                  def match = matcher[0]
                  if (match.size() < 2) {
                      break;
                  }
                  def attId = replica.attachments.find { it.filename?.equals(match[1]) }?.remoteId
                  if (!attId) {
                      log.error("""Could not find attachment with name ${match[1]}, 
                 known names: ${replica.attachments.filename}, 
                 match: ${replica.attachments.find { it.filename?.equals(match[1]) }}
             """)
                      str = str.replace(match[0], """<!-- inline processed image filename=#${match[1]}# -->""".toString())
                  } else {
                      def tmpStr = str.replace(match[0], """<img src="/secure/attachment/${attId}/${attId}_${match[1]}" />""".toString())
                      if (tmpStr == str) {
                          break;
                      }
                      str = tmpStr
                  }
                  counter++
              }
              str
          }
          def processNoImage = {
              //"<p><img
              // src=\"https://jira.smartodds.co.uk/images/icons/attach/noimage.png\"
              // imagetext=\"Screenshot from 2022-11-18 11-09-25.png|thumbnail\"
              // align=\"absmiddle\"
              // border=\"0\" /></p>"
              def counter = 0
              while (counter < 1000) {
                  def matcher = (str =~ /<img src="[^"]+" imagetext="(([^"]+)\|thumbnail)" align="absmiddle" border="0" \/>/)
                  if (matcher.size() < 1) {
                      break;
                  }
                  def match = matcher[0]
                  if (match.size() < 2) {
                      break;
                  }
                  def filename = match[2]
                  def attId = replica.attachments.find { it.filename?.equals(filename) }?.remoteId
                  if (!attId) {
                      log.error("""Could not find attachment with name ${filename}, 
                 known names: ${replica.attachments.filename}, 
                 match: ${replica.attachments.find { it.filename?.equals(filename) }}
             """)
                      str = str.replace(match[0], """<img src="/images/icons/attach/noimage.png" processed imagetext="$filename|thumbnail" align="absmiddle" border="0" />""".toString())
                  } else {
                      def tmpStr = str.replace(match[0], """<img src="/secure/attachment/${attId}/${attId}_${filename}" />""".toString())
                      if (tmpStr == str) {
                          break;
                      }
                      str = tmpStr
                  }
                  counter++
              }
              str
          }
          def processImgTagsWithIds = {
              //"<p>TEST DECS23456 </p> \n
              //<p><span class=\"image-wrap\" style=\"\"><img src=\"/rest/api/3/attachment/content/36820\"></span></p> \n
              //<p>TESt </p> \n
              //<p><span class=\"image-wrap\" style=\"\"><img src=\"/rest/api/3/attachment/content/36821\"></span></p> \n
              //<p>and more</p>"
              def counter = 0
              while (counter < 1000) {
                  def matcher = (str =~ /<img src="\/rest\/api\/3\/attachment\/content\/(\d+)">/)
                  if (matcher.size() < 1) {
                      return str
                  }
                  def match = matcher[0]
                  //println("match[1]=$match[1]")
                  if (match.size() < 2) { // match[0]=<img src="/rest/api/3/attachment/content/36820"> match[1]=36820
                      return str
                  }
                  def attId = match[1]
                  def attachment = replica.attachments.find { (it.remoteId as String) == ( attId as String ) }
                  if (!attachment) {
                      log.error("""Could not find attachment with id ${attId}, 
                 known ids: ${replica.attachments.remoteId}, 
                 match: ${replica.attachments.find { (it.remoteId as String) == ( attId as String ) }}
             """)
                      str = str.replace(match[0], """<img src="/rest/api/3/attachment/content/${attId}" processed />""".toString())
                  } else {
                      def tmpStr = str.replace(match[0], """<img src="/secure/attachment/${attId}/${attId}_${attachment.filename}" />""".toString())
                      if (tmpStr == str) {
                          break;
                      }
                      str = tmpStr
                  }
                  counter++
              }
              str
          }
          //log.error("#processimages 0 $str")
          str = processUnescapedLtGtTags()
          //log.error("#processimages 1 $str")
          str = processLtGtTags()
          //log.error("#processimages 2 $str")
          str = processNoImage()
          //log.error("#processimages 3 $str")
          str = processImgTagsWithIds()
          log.error("#processimages $str")
          str
      }
                  
      workItem.comments     = commentHelper.mergeComments(workItem, replica, {
          comment ->
      def attrAuthor = comment.author?.displayName ?: "Default-"
          comment.body =  "<b> ${attrAuthor} said:</b> " + comment.body
          comment.body = processInlineImages (comment.body)
      comment
      })
      
      
      
      
      workItem.description = processInlineImages(replica.description)


      Thank you.

      Kind regards,
      Mathieu Lepoutre

        CommentAdd your comment...
      1.  
        1
        0
        -1

        Hey, Bhakti Prasad Panda 
        Here's a video showing, how image processing works in my environment:

        Here's the script for Jira on-prem's Outgoing sync I'd used: 

        import com.exalate.transform.WikiToHtml
         
        replica.key            = issue.key
        replica.type           = issue.type
        replica.assignee       = issue.assignee
        replica.reporter       = issue.reporter
        replica.summary        = issue.summary
        replica.description    = WikiToHtml.transform(issue.description)
        //replica.description    = issue.description
        //replica.description    = nodeHelper.getHtmlField(issue, "description")
        replica.labels         = issue.labels
        replica.comments       = issue.comments
        replica.resolution     = issue.resolution
        replica.status         = issue.status
        replica.parentId       = issue.parentId
        replica.priority       = issue.priority
        replica.attachments    = issue.attachments
        replica.project        = issue.project
         
        //Comment these lines out if you are interested in sending the full list of versions and components of the source project.
        replica.project.versions = []
        replica.project.components = []
         
        replica.customFields."Severity" = issue.customFields."Severity"
        replica.customFields."Source" = issue.customFields."Source"
        replica.customFields."14357" = issue.customFields."14357"
        //replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria"
        replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria"
        replica.customFields."Story Points" = issue.customFields."Story Points"
         
        /*
        Custom Fields
         
        replica.customFields."CF Name" = issue.customFields."CF Name"
        */

        And here's the incoming script for ADO I'd used: 

        //workItem.labels       = replica.labels
        workItem.priority     = replica.priority
        if(firstSync){
           // Set type name from source entity, if not found set a default
           if(replica.type?.name=="Story"){
               workItem.typeName = "User Story";
           }else {
               workItem.typeName = nodeHelper.getIssueType(replica.type?.name)?.name ?: "Issue";
           }
        }
        def defaultUser = nodeHelper.getUserByEmail("jirasync@comaround.onmicrosoft.com")
        workItem.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
        workItem.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
        workItem.summary  = replica.summary
         
        workItem.attachments  = attachmentHelper.mergeAttachments(workItem, replica)
        
        store(issue)
        def await = { f -> scala.concurrent.Await$.MODULE$.result(f, scala.concurrent.duration.Duration.apply(1, java.util.concurrent.TimeUnit.MINUTES)) }
        def creds = await(httpClient.azureClient.getCredentials())
        def issueTrackerUrl = creds.issueTrackerUrl()
        def project = connection.trackerSettings.fieldValues."project"
        def processInlineImages = { str ->
            def processLtGtTags = {
                def counter = 0
                while (counter < 1000) {
                    def matcher = (str =~ /<!-- inline image filename=#(([^#]+)|(([^#]+)#([^#]+)))# -->/)
                    if (matcher.size() < 1) {
                        break;
                    }
                    def match = matcher[0]
                    if (match.size() < 2) {
                        break;
                    }
                    def filename = match[1]
                    def attId = workItem.attachments.find { it.filename?.equals(filename) }?.idStr
                    if (!attId) {
                        log.error("""Could not find attachment with name ${filename},
                   known names: ${replica.attachments.filename},
                   match: ${replica.attachments.find { it.filename?.equals(filename) }}
               """)
                        str = str.replace(match[0], """<!-- inline processed image filename=#${filename}# -->""".toString())
                    } else {
                        def tmpStr = str.replace(match[0], """<img src=\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=${filename}\"/>""".toString())
                        if (tmpStr == str) {
                            break;
                        }
                        str = tmpStr
                    }
                    counter++
                }
                str
            }
            def processNoImage = {
                //"<p><img
                // src=\"https://jira..../images/icons/attach/noimage.png\"
                // imagetext=\"Screenshot from 2022-11-18 11-09-25.png|thumbnail\"
                // align=\"absmiddle\"
                // border=\"0\" /></p>"
                def counter = 0
                while (counter < 1000) {
                    def matcher = (str =~ /<img src="[^"]+" imagetext="(([^"]+)\|thumbnail)" align="absmiddle" border="0" \/>/)
                    if (matcher.size() < 1) {
                        break;
                    }
                    def match = matcher[0]
                    if (match.size() < 2) {
                        break;
                    }
                    def filename = match[2]
                    def attId = workItem.attachments.find { it.filename?.equals(filename) }?.idStr
                    if (!attId) {
                        log.error("""Could not find attachment with name ${filename},
                   known names: ${replica.attachments.filename},
                   match: ${replica.attachments.find { it.filename?.equals(filename) }}
               """)
                        str = str.replace(match[0], """<img src="/images/icons/attach/noimage.png" processed imagetext="$filename|thumbnail" align="absmiddle" border="0" />""".toString())
                    } else {
                        /*
                        def m = "#<a href=\"https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#\" class=\"external-link\" target=\"_blank\" rel=\"nofollow noopener\">https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#</a>" =~ /#<a([^>]+)>([^<]+)#<\/a>/
                        m.iterator().next()[2]
                        */
                        //if (matchGroup1)
                        //debug.error("matchGroup0=${matchGroup0} matchGroup1=$matchGroup1")
                        def tmpStr = str.replaceAll(match[0], """<img src=\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=${filename}\"/>""".toString())
                        if (tmpStr == str) {
                            break;
                        }
                        str = tmpStr
                    }
                    counter++
                }
                str
            }
            if (str == null) {
                return null
            }
            str = processLtGtTags()
            str = processNoImage()
        
            log.error("#processimages $str")
            str
        }
        
        String value = processInlineImages(replica.description)
         
        if(replica.type?.name=="Bug"){
            workItem."Microsoft.VSTS.TCM.ReproSteps" = value
        }else{
            workItem.description  = value
        }
         
        //debug.error("description = ${workItem."Microsoft.VSTS.TCM.ReproSteps"} value=${value}")
        //workItem.description  = WikiToHtml.transform(replica.description)
        workItem.labels       = replica.labels
        //workItem.labels       = replica.labels.collect {it.label = it.label.replace("_", " "); it}
        //workItem.attachments  = attachmentHelper.mergeAttachments(workItem, replica)
        //workItem.iterationPath = replica.customFields."OTL code"?.value ?: default
        replica.changedComments = []
        //workItem.comments     = commentHelper.mergeComments(workItem, replica)
        workItem.comments     = commentHelper.mergeComments(workItem, replica, { c ->
            c.body = processInlineImages(c.body)
            c
        })
        //workItem.comments = replica.comments
        //debug.error(replica."Acceptance Criteria")
        workItem."ComAround.Outcome" = processInlineImages(replica."Acceptance Criteria".value)
         
        if(replica.customFields."Severity"?.value?.value!="1 - Severe"){
            if(replica.customFields."Severity"?.value?.value=="Medium"){
                workItem."Microsoft.VSTS.Common.Severity" = "3 - Medium"
            }else{
                workItem."Microsoft.VSTS.Common.Severity" = replica.customFields."Severity"?.value?.value  
            }
        }else{
            workItem."Microsoft.VSTS.Common.Severity" = "1 - Critical"
        }
         
        //workItem."Microsoft.VSTS.Common.Severity" = replica.customFields."Severity"?.value?.value
        /***/
        if(replica.customFields."Source"?.value?.value=="Customer" || replica.customFields."Source"?.value?.value=="Security" || replica.customFields."Source"?.value?.value=="Internal"){
            workItem.customFields."Source".value = replica.customFields."Source"?.value?.value
        }else{
            workItem.customFields."Source".value = "Internal"
        }
        workItem."comaround.ComAround.Customer" = replica.customFields."14357"?.value
        //workItem."ComAround.Outcome" = WikiToHtml.transform(replica.customFields."Acceptance Criteria"?.value)
        workItem."Microsoft.VSTS.Scheduling.StoryPoints" = replica.customFields."Story Points"?.value
        //debug.error(replica.customFields."Source"?.value?.value)
         
        def statusMapping = [
        "Open":"New",
        "Requirements Ready":"Ready",
        "In Progress":"Development",
        "Blocked":"Development",
        "Code Review":"Development",
        "Quality Review Failed":"Development",
        "Doneness Failed":"Development",
        "Reopened":"Development",
        "Quality Review":"Testing",
        "Doneness Review":"Verified",
        "Closed":"Closed",
        "Resolved":"Closed",
        "Reopened":"New"
        ]
        def remoteStatusName = replica.status?.name
        if(replica.type?.name=="Bug" || replica.type?.name=="Story"){
            workItem.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)   
        }
         
        /*
        Area Path Sync
        This also works for iterationPath field
         
        Set Area Path Manually
        workItem.areaPath = "Name of the project\\name of the area"
         
        Set Area Path based on remote side drop-down list
        Change "area-path-select-list" to actual custom field name
        workItem.areaPath = replica.customFields."area-path-select-list"?.value?.value
         
        Set Area Path based on remote side text field
        Change "area-path" to actual custom field name
        workItem.areaPath = replica.customFields."area-path".value
        */
         
        /*
        Status Synchronization
         
        Sync status according to the mapping [remote workItem status: local workItem status]
        If statuses are the same on both sides don"t include them in the mapping
        def statusMapping = ["Open":"New", "To Do":"Open"]
        def remoteStatusName = replica.status.name
        workItem.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)
        */



        Happy Exalating!

        Regards, Serhiy.

        1. Serhiy Onyshchenko

          Questions from Bhakti Prasad Panda 

          You have 2 methods in ADO incoming. One is "processInlineImages" and another is "processNoImage". Will both methods be in picture? If yes then how?

          only the processInlineImages will be called, and it will internally use the processLtGtTags and processNoImage , the function is to be used on any text field: 

          String value = processInlineImages(replica.description)
            
          if(replica.type?.name=="Bug"){
              workItem."Microsoft.VSTS.TCM.ReproSteps" = value
          }else{
              workItem.description  = value
          }
          
          

          and also

          workItem.comments     = commentHelper.mergeComments(workItem, replica, { c ->
              c.body = processInlineImages(c.body)
              c
          })
          I needed help in syncing one wiki field also for which i created this ticket. I can't see that field is mentioned in the incoming script of ADO. Please check and let me know.

           Let me shoot another video with a field example.

          Please expect an update by Thu Dec 15th 12:30 CET.
          Regards, Serhiy.

        2. Serhiy Onyshchenko

          Hey, Bhakti Prasad Panda , here's a video showing, how the same script is working for  images in a custom field:

          Outgoing in Jira:

          //...replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria"
          replica.customFields."Acceptance Criteria".value = WikiToHtml.transform(
          issue.customFields."Acceptance Criteria".value
          )


          Incoming to ADO: 

          //...
          def processInlineImages = { str ->
              def processLtGtTags = {
                  def counter = 0
                  while (counter < 1000) {
                      def matcher = (str =~ /<!-- inline image filename=#(([^#]+)|(([^#]+)#([^#]+)))# -->/)
                      if (matcher.size() < 1) {
                          break;
                      }
                      def match = matcher[0]
                      if (match.size() < 2) {
                          break;
                      }
                      def filename = match[1]
                      def attId = workItem.attachments.find { it.filename?.equals(filename) }?.idStr
                      if (!attId) {
                          log.error("""Could not find attachment with name ${filename},
                     known names: ${replica.attachments.filename},
                     match: ${replica.attachments.find { it.filename?.equals(filename) }}
                 """)
                          str = str.replace(match[0], """<!-- inline processed image filename=#${filename}# -->""".toString())
                      } else {
                          def tmpStr = str.replace(match[0], """<img src=\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=${filename}\"/>""".toString())
                          if (tmpStr == str) {
                              break;
                          }
                          str = tmpStr
                      }
                      counter++
                  }
                  str
              }
              def processNoImage = {
                  //"<p><img
                  // src=\"https://jira..../images/icons/attach/noimage.png\"
                  // imagetext=\"Screenshot from 2022-11-18 11-09-25.png|thumbnail\"
                  // align=\"absmiddle\"
                  // border=\"0\" /></p>"
                  def counter = 0
                  while (counter < 1000) {
                      def matcher = (str =~ /<img src="[^"]+" imagetext="(([^"]+)\|thumbnail)" align="absmiddle" border="0" \/>/)
                      if (matcher.size() < 1) {
                          break;
                      }
                      def match = matcher[0]
                      if (match.size() < 2) {
                          break;
                      }
                      def filename = match[2]
                      def attId = workItem.attachments.find { it.filename?.equals(filename) }?.idStr
                      if (!attId) {
                          log.error("""Could not find attachment with name ${filename},
                     known names: ${replica.attachments.filename},
                     match: ${replica.attachments.find { it.filename?.equals(filename) }}
                 """)
                          str = str.replace(match[0], """<img src="/images/icons/attach/noimage.png" processed imagetext="$filename|thumbnail" align="absmiddle" border="0" />""".toString())
                      } else {
                          /*
                          def m = "#<a href=\"https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#\" class=\"external-link\" target=\"_blank\" rel=\"nofollow noopener\">https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#</a>" =~ /#<a([^>]+)>([^<]+)#<\/a>/
                          m.iterator().next()[2]
                          */
                          //if (matchGroup1)
                          //debug.error("matchGroup0=${matchGroup0} matchGroup1=$matchGroup1")
                          def tmpStr = str.replaceAll(match[0], """<img src=\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=${filename}\"/>""".toString())
                          if (tmpStr == str) {
                              break;
                          }
                          str = tmpStr
                      }
                      counter++
                  }
                  str
              }
              if (str == null) {
                  return null
              }
              str = processLtGtTags()
              str = processNoImage()
           
              log.error("#processimages $str")
              str
          }
          
          workItem."Custom.HTML_Field" = processInlineImages(
              replica."Acceptance Criteria"
          )

          Regards, Serhiy.

        3. Serhiy Onyshchenko

          Hey, Bhakti Prasad Panda , I'd taken the liberty to take a look at a couple replicas synced in your tests, and the most recent I'd found was for https://jiraqa.bmc.com/browse/DRHD1-252 and it had the following text in the Acceptance Criteria: 

          From Jira
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat, sed consectetur magna porttitor.
          *(Hard return)* Nullam a leo ultricies, lobortis turpis a, commodo mauris. Curabitur accumsan, nulla vel aliquam porttitor, neque dolor tempor mi, quis pretium nisi diam sed dui.
          *(Soft return)* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat. 
           !^image.png|thumbnail!
           # Bullet point 1 
          # Bullet point 2 
          # Bullet point 3

          So the 

          !^image.png|thumbnail!

          was supposed to be replaced by this snippet in your outgoing: 

          replica.customFields."Acceptance Criteria".value = WikiToHtml.transform(
          issue.customFields."Acceptance Criteria".value
          )

          Is it added to the end of the Jira's outgoing sync script?
          Or maybe I'm looking at a wrong example of an issue, could you guide me to a more recent one, please?
          Regards, Serhiy.

        CommentAdd your comment...
      2.  
        1
        0
        -1

        Hello Serhiy Onyshchenko ,


        Please find the attached zip containing the configuration done on Azure and Jira end:


        Exalate_Config.zip


        We will wait for you response.


        Regards
        Bhakti

        1. Serhiy Onyshchenko

          Thanks, Bhakti Prasad Panda , checking the config now to see, what could be going on with those images...

        2. Serhiy Onyshchenko

          Here's the config from the zip in a side-by-side view (feel free to scroll):


          ADOJira
          ​Out

          replica.key            = workItem.key
          replica.assignee       = workItem.assignee 
          replica.summary        = workItem.summary
          if(workItem.type.name=="Bug"){
              replica.description = workItem."Microsoft.VSTS.TCM.ReproSteps"
              //debug.error(replica.description)
          }else{
              replica.description = workItem.description
          }
          
          //replica.description    = workItem.description
          replica.type           = workItem.type
          replica.status         = workItem.status
          //debug.error(replica.status)
          //debug.error(workItem.status)
          replica.labels         = workItem.labels
          replica.priority       = workItem.priority
          //replica.comments       = nodeHelper.stripHtmlFromComments(workItem.comments)
          replica.comments       = workItem.comments
          replica.attachments    = workItem.attachments
          replica.project        = workItem.project
          replica.areaPath       = workItem.areaPath
          replica.reporter       = workItem.reporter
          replica.iterationPath  = workItem.iterationPath
          //replica.customFields."Severity" = workItem.customFields."Severity"
          replica.customKeys."Severity"  = workItem."Microsoft.VSTS.Common.Severity"
          replica.customFields."Source" = workItem.customFields."Source"
          //replica.customKeys."Outcome"  = nodeHelper.stripHtml(workItem."ComAround.Outcome")
          replica.customKeys."Outcome"  = workItem."ComAround.Outcome"
          replica.customKeys."Customer" = workItem."comaround.ComAround.Customer"
          replica.customKeys."Story Points" = workItem."Microsoft.VSTS.Scheduling.StoryPoints"
          
          //Send a Custom Field value
          //replica.customFields."CF Name" = workItem.customFields."CF Name"

          import com.exalate.transform.WikiToHtml
          
          replica.key            = issue.key
          replica.type           = issue.type
          replica.assignee       = issue.assignee
          replica.reporter       = issue.reporter
          replica.summary        = issue.summary
          replica.description    = WikiToHtml.transform(issue.description)
          //replica.description    = issue.description
          //replica.description    = nodeHelper.getHtmlField(issue, "description")
          replica.labels         = issue.labels
          replica.comments       = issue.comments
          replica.resolution     = issue.resolution
          replica.status         = issue.status
          replica.parentId       = issue.parentId
          replica.priority       = issue.priority
          replica.attachments    = issue.attachments
          replica.project        = issue.project
          
          //Comment these lines out if you are interested in sending the full list of versions and components of the source project. 
          replica.project.versions = []
          replica.project.components = []
          
          replica.customFields."Severity" = issue.customFields."Severity"
          replica.customFields."Source" = issue.customFields."Source"
          replica.customFields."14357" = issue.customFields."14357"
          //replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria"
          replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria"
          replica.customFields."Story Points" = issue.customFields."Story Points"
          
          /*
          Custom Fields
          
          replica.customFields."CF Name" = issue.customFields."CF Name"
          */
          In
          //workItem.labels       = replica.labels
          workItem.priority     = replica.priority
          if(firstSync){
             // Set type name from source entity, if not found set a default
             if(replica.type?.name=="Story"){
                 workItem.typeName = "User Story";
             }else {
                 workItem.typeName = nodeHelper.getIssueType(replica.type?.name)?.name ?: "Issue";
             }
          }
          def defaultUser = nodeHelper.getUserByEmail("jirasync@comaround.onmicrosoft.com")
          workItem.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
          workItem.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
          workItem.summary  = replica.summary
          
          workItem.attachments  = attachmentHelper.mergeAttachments(workItem, replica)
          
          store(issue)
          def await = { f -> scala.concurrent.Await$.MODULE$.result(f, scala.concurrent.duration.Duration.apply(1, java.util.concurrent.TimeUnit.MINUTES)) }
          def creds = await(httpClient.azureClient.getCredentials())
          def issueTrackerUrl = creds.issueTrackerUrl()
          def project = connection.trackerSettings.fieldValues."project"
          def imageReplace = { desc ->
              def _desc = desc
              def descMatcher = _desc =~ /<!-- inline image filename=#([^#]+)# -->/
              def matches = descMatcher.size()
          
              for (def i=0; i<matches; i++) {
                  if (!descMatcher.iterator().hasNext()) {
                      return _desc
                  }
                  def match = descMatcher.iterator().next();
                  def matchGroup0 = match[0]
                  def matchGroup1 = match[1]
                  def linkMatcher = matchGroup1 =~ /<a href="([^"]+)" ([^\/]+)\/>/
                  /*
                  
          ​def m = "#<a href=\"https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#\" class=\"external-link\" target=\"_blank\" rel=\"nofollow noopener\">https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#</a>" =~ /#<a([^>]+)>([^<]+)#<\/a>/
          m.iterator().next()[2]
          */
                  //if (matchGroup1)
                  //debug.error("matchGroup0=${matchGroup0} matchGroup1=$matchGroup1")
                  def attId = issue.attachments.find { a -> a.filename == matchGroup1 }?.idStr
                  if (attId) {
                      //$issueTrackerUrl/$organizationName/$projectId/_apis/wit/attachments/$localId?fileName=$attName
                      _desc = _desc.replaceAll(matchGroup0, "<img src=\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=$matchGroup1\"/>")
                  }
              }
              _desc
          }
          
          String value = imageReplace(replica.description)
          
          if(replica.type?.name=="Bug"){
              workItem."Microsoft.VSTS.TCM.ReproSteps" = value
          }else{
              workItem.description  = value
          }
          
          //debug.error("description = ${workItem."Microsoft.VSTS.TCM.ReproSteps"} value=${value}")
          //workItem.description  = WikiToHtml.transform(replica.description)
          workItem.labels       = replica.labels
          //workItem.labels       = replica.labels.collect {it.label = it.label.replace("_", " "); it}
          //workItem.attachments  = attachmentHelper.mergeAttachments(workItem, replica)
          //workItem.iterationPath = replica.customFields."OTL code"?.value ?: default
          replica.changedComments = []
          //workItem.comments     = commentHelper.mergeComments(workItem, replica)
          workItem.comments     = commentHelper.mergeComments(workItem, replica, { c ->
              c.body = imageReplace(c.body)
              c
          })
          //workItem.comments = replica.comments
          //debug.error(replica."Acceptance Criteria")
          workItem."ComAround.Outcome" = imageReplace(replica."Acceptance Criteria".value)
          
          if(replica.customFields."Severity"?.value?.value!="1 - Severe"){
              if(replica.customFields."Severity"?.value?.value=="Medium"){
                  workItem."Microsoft.VSTS.Common.Severity" = "3 - Medium"
              }else{
                  workItem."Microsoft.VSTS.Common.Severity" = replica.customFields."Severity"?.value?.value   
              }
          }else{
              workItem."Microsoft.VSTS.Common.Severity" = "1 - Critical"
          }
          
          //workItem."Microsoft.VSTS.Common.Severity" = replica.customFields."Severity"?.value?.value
          /***/
          if(replica.customFields."Source"?.value?.value=="Customer" || replica.customFields."Source"?.value?.value=="Security" || replica.customFields."Source"?.value?.value=="Internal"){
              workItem.customFields."Source".value = replica.customFields."Source"?.value?.value
          }else{
              workItem.customFields."Source".value = "Internal"
          }
          workItem."comaround.ComAround.Customer" = replica.customFields."14357"?.value
          //workItem."ComAround.Outcome" = WikiToHtml.transform(replica.customFields."Acceptance Criteria"?.value)
          workItem."Microsoft.VSTS.Scheduling.StoryPoints" = replica.customFields."Story Points"?.value
          //debug.error(replica.customFields."Source"?.value?.value)
          
          def statusMapping = [
          "Open":"New",
          "Requirements Ready":"Ready",
          "In Progress":"Development",
          "Blocked":"Development",
          "Code Review":"Development",
          "Quality Review Failed":"Development",
          "Doneness Failed":"Development",
          "Reopened":"Development",
          "Quality Review":"Testing",
          "Doneness Review":"Verified",
          "Closed":"Closed",
          "Resolved":"Closed",
          "Reopened":"New"
          ]
          def remoteStatusName = replica.status?.name
          if(replica.type?.name=="Bug" || replica.type?.name=="Story"){
              workItem.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)    
          }
          
          /*
          Area Path Sync
          This also works for iterationPath field
          
          Set Area Path Manually
          workItem.areaPath = "Name of the project\\name of the area"
          
          Set Area Path based on remote side drop-down list
          Change "area-path-select-list" to actual custom field name
          workItem.areaPath = replica.customFields."area-path-select-list"?.value?.value
          
          Set Area Path based on remote side text field
          Change "area-path" to actual custom field name
          workItem.areaPath = replica.customFields."area-path".value
          */
          
          /*
          Status Synchronization
          
          Sync status according to the mapping [remote workItem status: local workItem status]
          If statuses are the same on both sides don"t include them in the mapping
          def statusMapping = ["Open":"New", "To Do":"Open"]
          def remoteStatusName = replica.status.name
          workItem.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)
          */
          import com.exalate.transform.HtmlToWiki
          
          if(firstSync){
             issue.projectKey   = "DRHD1" 
             // Set type name from source issue, if not found set a default
             if(replica.type?.name=="User Story"){
                 issue.typeName = "Story"
             }else{
                 issue.typeName = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Feedback"
             }
          }
          def defaultUser = nodeHelper.getUserByFullName("DBA JIRA sync user")
          issue.reporter     = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
          issue.assignee     = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
          issue.summary      = replica.summary
          //issue.description  = replica.description
          HtmlToWiki htw   = new HtmlToWiki()
          issue.description  = htw.transform(replica.description)
          //issue.comments     = commentHelper.mergeComments(issue, replica){ it.executor = nodeHelper.getUserByEmail(it.author?.email) ?: defaultUser }
          issue.attachments  = attachmentHelper.mergeAttachments(issue, replica)
          //issue.labels       = replica.labels
          issue.labels       = replica.labels.collect { it.label = it.label.trim().replace(" ", "_"); it }
          issue.comments     = commentHelper.mergeComments(issue, replica, {
              
              comment ->
              comment.body = htw.transform(comment.body)
          })
          
          //debug.error(replica.customKeys."Severity")
          //issue.customFields."Severity".value = replica.customFields."Severity".value
          if(replica.customKeys."Severity"!="1 - Critical"){
              issue.customFields."Severity".value = replica.customKeys."Severity"
          }else{
              issue.customFields."Severity".value = "1 - Severe"
          }
          
          issue.customFields."Source".value = replica.customFields."Source".value
          issue.customFields."14357".value = replica.customKeys."Customer"
          //issue.customFields."Acceptance Criteria".value = replica.customKeys."Outcome"
          issue.customFields."Acceptance Criteria".value = htw.transform(replica.customKeys."Outcome")
          
          if(replica.type?.name=="User Story"){
              issue.customFields."Story Points".value = replica.customKeys."Story Points"
          }
          //issue.customFields."Story Points".value = replica.customKeys."Story Points"
          
          //debug.error(replica.status.name)
          
          def statusMapping = [
          "New":"Open",
          "Ready":"Requirements Ready",
          "Development":"In Progress",
          "Testing":"Quality Review",
          "Verified":"Doneness Review",
          "Implemented":"Doneness Review",
          "Closed":"Closed"
          ]
          def remoteStatusName = replica.status?.name
          if(replica.status?.name=="New"){
              if(issue.status!=null && issue.status.name=="Closed"){
                  issue.setStatus("Reopened")
              }else{
                  if(replica.type?.name=="Bug" || replica.type?.name=="User Story"){
                      issue.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)     
                  }
              }
          }else{
              if(replica.type?.name=="Bug" || replica.type?.name=="User Story"){
                  issue.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)     
              }
          }
          
          
          
          /*
          User Synchronization (Assignee/Reporter)
          
          Set a Reporter/Assignee from the source side, if the user can't be found set a default user
          You can use this approach for custom fields of type User
          def defaultUser = nodeHelper.getUserByEmail("default@idalko.com")
          issue.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
          issue.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
          */
          
          /*
          Comment Synchronization
          
          Sync comments with the original author if the user exists in the local instance
          Remove original Comments sync line if you are using this approach
          issue.comments = commentHelper.mergeComments(issue, replica){ it.executor = nodeHelper.getUserByEmail(it.author?.email) }
          */
          
          /*
          Status Synchronization
          
          Sync status according to the mapping [remote issue status: local issue status]
          If statuses are the same on both sides don't include them in the mapping
          def statusMapping = ["Open":"New", "To Do":"Backlog"]
          def remoteStatusName = replica.status.name
          issue.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)
          */
          
          /*
          Custom Fields
          
          This line will sync Text, Option(s), Number, Date, Organization, and Labels CFs
          For other types of CF check documentation
          issue.customFields."CF Name".value = replica.customFields."CF Name".value
          */

          Here are the relevant rules when syncing description from ADO to Jira:

          1. ADO Out:

            if(workItem.type.name=="Bug"){
                replica.description = workItem."Microsoft.VSTS.TCM.ReproSteps"
                //debug.error(replica.description)
            }else{
                replica.description = workItem.description
            }
            
            
          2. Jira In:

            HtmlToWiki htw   = new HtmlToWiki()
            issue.description  = htw.transform(replica.description)

          Whenever a description with HTML like:

          <div><p style="box-sizing:border-box;margin:0px;color:rgba(0, 0, 0, 0.9);"><span style="box-sizing:border-box;">From Azure</span>
          </p>
              <p style="box-sizing:border-box;margin:0px;color:rgba(0, 0, 0, 0.9);"><span style="box-sizing:border-box;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat, sed consectetur magna porttitor.</span>
              </p>
              <p style="box-sizing:border-box;margin:0px;color:rgba(0, 0, 0, 0.9);"><strong style="">(Hard return)</strong><span
                      style=""> Nullam a leo ultricies, lobortis turpis a, commodo mauris. Curabitur accumsan, nulla vel aliquam porttitor, neque dolor tempor mi, quis pretium nisi diam sed dui.<br></span><strong
                      style="">(Soft return)</strong><span style=""> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat.</span>
              </p><br></div>
          <div><img
                  src="https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9…ents/1d43effa-dbe9-443b-a8e9-6d9413198143?fileName=image.png"
                  alt=Image><br></div>
          <div>
              <ol style="box-sizing:border-box;margin:0px;padding-left:40px;color:rgba(0, 0, 0, 0.9);">
                  <li style="box-sizing:border-box;">Bullet point 1</li>
                  <li style="box-sizing:border-box;">Bullet point 2</li>
                  <li style="box-sizing:border-box;">Bullet point 3</li>
              </ol>
          </div>

          The exalate transformers would try to tackle each html element separately:
          each <p>  will result in a line break after the text inside it
          each <ol>  will give a #-based list
          and <img> would simply be ignored.

          Luckily, this article https://community.servicenow.com/community?id=community_article&sys_id=5566161fdb3264103daa1ea668961981
          suggests that

          HtmlToWiki htw   = new HtmlToWiki(replica.attachments)
          issue.description  = htw.transform(replica.description)

          Should do something different.


          Please, give it a try, and let me know how it goes.
          Regards, Serhiy.

        CommentAdd your comment...
      3.  
        1
        0
        -1

        Hi Bhakti Prasad Panda

        Thanks for raising this question, in order for you to make sure this works properly please use the following snippets. 

        Jira Outgoing Sync:

        replica.description = nodeHelper.stripHtml(issue.description)


        Jira incoming script:

        //Place the following lines on top of your code:
        import com.exalate.transform.HtmlToWiki
        HtmlToWiki htw   = new HtmlToWiki()
        
        //Transforming description from Azure (HTML) to Jira (Wiki)
        issue.description  = htw.transform(replica.description)


        After verifying the code is correct in your side, please let us know the outcome, thanks for choosing our product for your best interest.

        Best regards,
        Jose Pablo

        1. Bhakti Prasad Panda

          Hello Jose,


          I am not sure whether you read my question properly. I suggest please read before answering. We already tried using above script in Jira incoming and its not working properly. Strange thing is it works sometimes and most of the times it doesn't.


          Regards

          Bhakti

        2. Ariel Aguilar

          Hi Bhakti,

          Isn't really useful to indicate sometimes it works, and sometimes it doesn't, can you be more specific what is not working and in what cases? I believe Jose just made sure you are setting up the transformers correctly.

          Kind regards,

          Ariel

        3. Bhakti Prasad Panda

          Hello Ariel,


          When we sync description or any similar field except comments from Azure to Jira, the images in that text gets lost when comes to Jira. Please see the attached image which i added in my question.

          So basically it is loosing the image in between. My question is why is it happening?

          The code mentioned by Jose is already in use and using only that we are able to sync some part of description.


          I hope its clear for you now.


          Regards
          Bhakti

        4. Serhiy Onyshchenko

          Hey, Bhakti Prasad Panda , I'll set this up and give you an update this Thursday Dec 1st.

        CommentAdd your comment...