Questions for Confluence license has expired.

Please purchase a new license to continue using Questions for Confluence.

Need a solution for a bi-directional sync, of embedded (in-line) images in the description/comments/Text fields etc.) and their attachments

 
1
0
-1

Hello support.

Main goal is to be able to create a bi-directional sync. from Azure Devops Board to Jira.

Here the first issues that I have so far:

Context: using Exalate in Script mode From Azure Work item(Outgoing sync ) to Jira Cloud Issue (Incoming sync)  ( note: as a start we need Azure to Jira sync. but our main goal is to have bi-directional sync )

To reproduce my situation:

  • From Azure Board create a work item
    • in the Description add some text 
      • Add an image by pasting from a print screen
      • Save
    • Reopen the same work item
    • add a Comment 
      • Add an new image by pasting from a new print screen
      • Save

Azure DevOps Board - Exalate outbound


// Azure DevOps Board - Exalate outbound

replica.description    = workItem.description	// Note description will contain HTML

replica.attachments    = workItem.attachments

replica.comments       = workItem.comments 		// Note comment will contain HTML

NOTE from the Jira Exalate connection information of the Remote replica entity section about the Attachments

"attachments": [
      {
        "id": "5729801f-8a84-4dd6-adc7-7521854a47d2",
        "filename": "image.png",
        "internal": false,
        "zip": false
      },
      {
        "id": "fbc509af-2794-4766-b347-99a7fd7bd688",
        "filename": "image.png",
        "internal": false,
        "zip": false
      }
    ],

Jira Cloud - Exalate Incoming sync

// Jira Cloud - Exalate Incoming sync

import com.exalate.transform.HtmlToWiki


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

HtmlToWiki htwDesc   = new HtmlToWiki(replica.attachments)
String wikiDescriptionText  = htwDesc.transform(replica.description)  // replica as HTML Jira understands wiki
issue.description = wikiDescriptionText 


HtmlToWiki htwComment   = new HtmlToWiki(replica.attachments) //  // replica as HTML Jira understands wiki 
issue.comments = commentHelper.mergeComments(issue, replica,  {
    comment ->    
    comment.body = htwComment.transform(comment.body)
    comment
})

The result in the Jira issue, both the description and comment text fields have the same image (The last image added to the comment, the second image.png). 

I did try the suggested solution from : https://community.exalate.com/display/exacom/questions/20124163/exalating-attachments-pasted-images-on-azuredevops-result-in-same-name-files-on-receiving-jira-side

Solution to rename the filename values in replica.attachments but the HtmlToWiki::processImage methode/function base it self on the HTML content only to scans for filename in the Image 'src' attribute. Giving me a broken in-line image link on the Jira side.

Since the Field content (HTML) (description and/or comments) in-line images information need/must fit the attachment information in order to resolve the image information to display in the proper context. There is a missing part to the solution.

Hypotheses:

So by renaming the images from azure to Jira …in Jira new filename ….when will do the other sync Jira to Azure inbound script even using the attachmentHelper.mergeAttachments tool … are those images are added to the work item making duplicates because their is different filenames.

Main goal is to be able to create a bi-directional sync. from

  • Azure Devops Board
  • Jira Cloud

Help would be appreciated, 

    CommentAdd your comment...

    1 answer

    1.  
      1
      0
      -1

      Hello,



      Add the following code on your Outgoing Sync on Jira Cloud

      replica.comments = nodeHelper.getHtmlComments(issue)



      Incoming Sync Azure DevOps

      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)
      
      



      Kind Regards,

      Javier Pozuelo

        CommentAdd your comment...