Uploaded image for project: 'Jira Data Center'
  1. Jira Data Center
  2. JRASERVER-61571

CSV Importer should allow user to choose to import Issue links as Outward or Inward Description.

    • Icon: Suggestion Suggestion
    • Resolution: Unresolved
    • None
    • None
    • None
    • 226
    • 5
    • We collect Jira feedback from various sources, and we evaluate what we've collected when planning our product roadmap. To understand how this piece of feedback will be reviewed, see our Implementation of New Features Policy.

      NOTE: This suggestion is for JIRA Server. Using JIRA Cloud? See the corresponding suggestion.

      Problem Definition

      When performing a CSV Import, the only issue links that can be created are in the form of Outward Descriptions (eg. X blocks Y). However, there is no way to link in the form of Inward Descriptions (eg. X is blocked by Y). This is a hassle for users who wants to import new issues through CSV that have inward description links to existing issues.

      Suggested Solution

      Allow the user to pick whether the new links formed are in the form of Outward or Inward Descriptions.

          Form Name

            [JRASERVER-61571] CSV Importer should allow user to choose to import Issue links as Outward or Inward Description.

            +1 

            Szymon Smerża added a comment - +1 

            k.kapur added a comment -

            Would really love to have this feature for user to select between outward and inward links

            k.kapur added a comment - Would really love to have this feature for user to select between outward and inward links

            Hi Everyone, 
            Run below Script in console to do Bulk Change using JQL. The below script is modified from the Adaptavist original single  Issue key code.

            • package com.adaptavist
              import com.atlassian.jira.bc.issue.search.SearchService
              import com.atlassian.jira.issue.Issue
              import com.atlassian.jira.issue.link.IssueLinkManager
              import com.atlassian.jira.issue.search.SearchResults
              import com.atlassian.jira.user.ApplicationUser
              import com.atlassian.jira.web.bean.PagerFilter
              import groovy.transform.Field
              // Reverse link direction of all issue links of a given issue link type for a given issue
              import groovy.xml.MarkupBuilder
              import com.atlassian.jira.component.ComponentAccessor
              import com.atlassian.jira.config.properties.APKeys
              import com.atlassian.jira.issue.link.IssueLinkTypeManager
              import com.atlassian.jira.issue.link.IssueLink
              import com.atlassian.jira.issue.link.IssueLinkType
              import com.onresolve.scriptrunner.parameters.annotation.IssueLinkTypePicker
              import com.onresolve.scriptrunner.parameters.annotation.*
              import com.onresolve.scriptrunner.parameters.annotation.meta.Option
              // Get the components
              def issueManager = ComponentAccessor.issueManager
              def issueLinkManager = ComponentAccessor.issueLinkManager
              def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
              def issueLinkTypeManager = ComponentAccessor.getComponent(IssueLinkTypeManager)
              @Select(label = "Issue Key or Jql", description = "Select to do a IssueKey or Jql Search", options = [@Option(label = "Issue Key", value = "key"),@Option(label = "JQL", value = "jql")])
              String jqlOrKey
              @ShortTextInput(label = "Issue key", description = "Enter a issue key")
              String issueKey
              @ShortTextInput(label = "Jql Search", description = "Enter a valid JQL")
              String jqlSearchString
              @IssueLinkTypePicker(label = 'Issue link type', description = 'Pick an issue link type', placeholder = 'Pick an issue link type')
              IssueLinkType issueLinkType
              @Checkbox(label = "Reverse inwards links", description = "Check to reverse inwards links")
              Boolean inwardLinksShouldBeReversed
              @Checkbox(label = "Reverse outwards links", description = "Check to reverse outwards links")
              Boolean outwardLinksShouldBeReversed
              @Checkbox(label = "Preview", description = "Check for dry run. If Checked, nothing will actually be changed")
              Boolean preview
              def results = new ArrayList<>();
              Issue issue = null;
              if(jqlOrKey != null) {
                  if (jqlOrKey == "jql" && jqlSearchString != null) {
                      def issues = jqlSearch(jqlSearchString, loggedInUser, 0)?.getResults()
                      if(issues != null) {
                          issues.each { Issue thisIssue ->
                              results.add(processIssue(issueLinkManager, thisIssue, issueLinkType, issueKey, inwardLinksShouldBeReversed, outwardLinksShouldBeReversed, preview, loggedInUser))
                          }
                          return results
                      }
                  }
              // Get the issue
                  if(jqlOrKey == "key") {
                      issue = issueManager.getIssueByCurrentKey(issueKey)
                      assert issue: "no issue found for '${issueKey}'"
                      // Get the issue links to other issues
                      return processIssue(issueLinkManager, issue, issueLinkType, issueKey, inwardLinksShouldBeReversed, outwardLinksShouldBeReversed, preview, loggedInUser)
                  }
              }
              private String processIssue(IssueLinkManager issueLinkManager, Issue issue, IssueLinkType issueLinkType, String issueKey, boolean inwardLinksShouldBeReversed, boolean outwardLinksShouldBeReversed, boolean preview, loggedInUser) {
                  log.warn("Processing issue of ${issue}")
                  def inwardLinks = issueLinkManager.getInwardLinks(issue.id).findAll { it.issueLinkType == issueLinkType }
                  def outwardLinks = issueLinkManager.getOutwardLinks(issue.id).findAll { it.issueLinkType == issueLinkType }
                  final Long sequence = 1L
                  def writer = new StringWriter()
                  def builder = new MarkupBuilder(writer)
                  builder.style(type: "text/css",
                          '''
                   #output, #output td, #output th{
                          border: 1px solid black;
                          padding: 1em;
                      }
                      #output{
                          border-collapse: collapse;
                      }
                      th {
                          background: lightgray;
                      }
                  ''')
                  builder.p {
                      p "${inwardLinks.size()} incoming links and ${outwardLinks.size()} outgoing links found for issue ${issueKey} and link type ${issueLinkType.name}."
                      p "Inward links will ${inwardLinksShouldBeReversed ? '' : 'not'} be reversed"
                      p "Outward links will ${outwardLinksShouldBeReversed ? '' : 'not'} be reversed"
                      p { i "${preview ? 'DRY RUN - nothing will be changed' : 'changes will be applied'}" }
                      table(id: "output") {
                          tr {
                              th "Original link"
                              th "new reversed link"
                          }
                          // reverse link direction for inwards links
                          if (inwardLinksShouldBeReversed) {
                              def newSourceIssue = issue
                              inwardLinks.each { IssueLink link ->
                                  def newDestinationIssue = link.sourceObject
                                  tr {
                                      td { // Original link
                                          a href: urlify(link.destinationObject.key), link.destinationObject.key
                                          i link.getIssueLinkType().getInward()
                                          a href: urlify(link.sourceObject.key), link.sourceObject.key
                                      }
                                      td { // new reversed link
                                          a href: urlify(newDestinationIssue.key), newDestinationIssue.key
                                          i issueLinkType.getInward()
                                          a href: urlify(newSourceIssue.key), newSourceIssue.key
                                      }
                                  }
                                  if (!preview) {
                                      log.warn "reversing existing link '${link.sourceObject.key} ${link.getIssueLinkType().getOutward()} ${link.destinationObject.key}'"
                                      issueLinkManager.createIssueLink(newSourceIssue.id, newDestinationIssue.id, issueLinkType.id, sequence, loggedInUser)
                                      issueLinkManager.removeIssueLink(link, loggedInUser)
                                  }
                              }
                          }
                          // reverse link direction for outwards links
                          if (outwardLinksShouldBeReversed) {
                              def newDestinationIssue = issue
                              outwardLinks.each { IssueLink link ->
                                  def newSourceIssue = link.destinationObject
                                  tr {
                                      td { // Original link
                                          a href: urlify(link.sourceObject.key), link.sourceObject.key
                                          i link.getIssueLinkType().getOutward()
                                          a href: urlify(link.destinationObject.key), link.destinationObject.key
                                      }
                                      td { // new reversed link
                                          a href: urlify(newSourceIssue.key), newSourceIssue.key
                                          i issueLinkType.getOutward()
                                          a href: urlify(newDestinationIssue.key), newDestinationIssue.key
                                      }
                                  }
                                  if (!preview) {
                                      issueLinkManager.createIssueLink(newSourceIssue.id, newDestinationIssue.id, issueLinkType.id, sequence, loggedInUser)
                                      issueLinkManager.removeIssueLink(link, loggedInUser)
                                      log.warn "reversing existing link '${link.sourceObject.key} ${link.getIssueLinkType().getOutward()} ${link.destinationObject.key}'"
                                  }
                              }
                          }
                      }
                  }
                  return writer.toString()
              }
              static def urlify(issuekey) {
                  def baseUrl = ComponentAccessor.applicationProperties.getString(APKeys.JIRA_BASEURL)
                  return "${baseUrl}/browse/${issuekey}"
              }
              SearchResults<Issue> jqlSearch(String jqlString, ApplicationUser user, int index) {
                  try {
                      int pageSize = 10000;
                      //log.debug("performing search with jql $jqlString");
                      SearchService searchService = ComponentAccessor.getComponent(SearchService.class);
                      SearchService.ParseResult parseResult = searchService.parseQuery(user, jqlString);
                      if (parseResult.isValid()) {
                          SearchResults<Issue> results = searchService.search(user, parseResult.getQuery(), PagerFilter.newPageAlignedFilter(index, pageSize));
                          return results;
                      }
                  } catch (Exception e) {
                      log.warn("Error into JQL Search with jqlString ${jqlString}");
                  }
                  return null;
              } 

              This helps a lot and make your work easy

            Naga Venkata Uday Kiran Kalangi added a comment - - edited Hi Everyone,  Run below Script in console to do Bulk Change using JQL. The below script is modified from the Adaptavist original single  Issue key code. package com.adaptavist import com.atlassian.jira.bc.issue.search.SearchService import com.atlassian.jira.issue.Issue import com.atlassian.jira.issue.link.IssueLinkManager import com.atlassian.jira.issue.search.SearchResults import com.atlassian.jira.user.ApplicationUser import com.atlassian.jira.web.bean.PagerFilter import groovy.transform.Field // Reverse link direction of all issue links of a given issue link type for a given issue import groovy.xml.MarkupBuilder import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.config.properties.APKeys import com.atlassian.jira.issue.link.IssueLinkTypeManager import com.atlassian.jira.issue.link.IssueLink import com.atlassian.jira.issue.link.IssueLinkType import com.onresolve.scriptrunner.parameters.annotation.IssueLinkTypePicker import com.onresolve.scriptrunner.parameters.annotation.* import com.onresolve.scriptrunner.parameters.annotation.meta.Option // Get the components def issueManager = ComponentAccessor.issueManager def issueLinkManager = ComponentAccessor.issueLinkManager def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser def issueLinkTypeManager = ComponentAccessor.getComponent(IssueLinkTypeManager) @Select(label = "Issue Key or Jql" , description = "Select to do a IssueKey or Jql Search" , options = [@Option(label = "Issue Key" , value = "key" ),@Option(label = "JQL" , value = "jql" )]) String jqlOrKey @ShortTextInput(label = "Issue key" , description = "Enter a issue key" ) String issueKey @ShortTextInput(label = "Jql Search" , description = "Enter a valid JQL" ) String jqlSearchString @IssueLinkTypePicker(label = 'Issue link type' , description = 'Pick an issue link type' , placeholder = 'Pick an issue link type' ) IssueLinkType issueLinkType @Checkbox(label = "Reverse inwards links" , description = "Check to reverse inwards links" ) Boolean inwardLinksShouldBeReversed @Checkbox(label = "Reverse outwards links" , description = "Check to reverse outwards links" ) Boolean outwardLinksShouldBeReversed @Checkbox(label = "Preview" , description = "Check for dry run. If Checked, nothing will actually be changed" ) Boolean preview def results = new ArrayList<>(); Issue issue = null ; if (jqlOrKey != null ) {     if (jqlOrKey == "jql" && jqlSearchString != null ) {         def issues = jqlSearch(jqlSearchString, loggedInUser, 0)?.getResults()         if (issues != null ) {             issues.each { Issue thisIssue ->                 results.add(processIssue(issueLinkManager, thisIssue, issueLinkType, issueKey, inwardLinksShouldBeReversed, outwardLinksShouldBeReversed, preview, loggedInUser))             }             return results         }     } // Get the issue     if (jqlOrKey == "key" ) {         issue = issueManager.getIssueByCurrentKey(issueKey)         assert issue: "no issue found for '${issueKey}' "         // Get the issue links to other issues         return processIssue(issueLinkManager, issue, issueLinkType, issueKey, inwardLinksShouldBeReversed, outwardLinksShouldBeReversed, preview, loggedInUser)     } } private String processIssue(IssueLinkManager issueLinkManager, Issue issue, IssueLinkType issueLinkType, String issueKey, boolean inwardLinksShouldBeReversed, boolean outwardLinksShouldBeReversed, boolean preview, loggedInUser) {     log.warn( "Processing issue of ${issue}" )     def inwardLinks = issueLinkManager.getInwardLinks(issue.id).findAll { it.issueLinkType == issueLinkType }     def outwardLinks = issueLinkManager.getOutwardLinks(issue.id).findAll { it.issueLinkType == issueLinkType }     final Long sequence = 1L     def writer = new StringWriter()     def builder = new MarkupBuilder(writer)     builder.style(type: "text/css" ,             '''      #output, #output td, #output th{             border: 1px solid black;             padding: 1em;         }         #output{             border-collapse: collapse;         }         th {             background: lightgray;         }     ''')     builder.p {         p "${inwardLinks.size()} incoming links and ${outwardLinks.size()} outgoing links found for issue ${issueKey} and link type ${issueLinkType.name}."         p "Inward links will ${inwardLinksShouldBeReversed ? '' : ' not'} be reversed"         p "Outward links will ${outwardLinksShouldBeReversed ? '' : ' not'} be reversed"         p { i "${preview ? 'DRY RUN - nothing will be changed' : 'changes will be applied' }" }         table(id: "output" ) {             tr {                 th "Original link"                 th " new reversed link"             }             // reverse link direction for inwards links             if (inwardLinksShouldBeReversed) {                 def newSourceIssue = issue                 inwardLinks.each { IssueLink link ->                     def newDestinationIssue = link.sourceObject                     tr {                         td { // Original link                             a href: urlify(link.destinationObject.key), link.destinationObject.key                             i link.getIssueLinkType().getInward()                             a href: urlify(link.sourceObject.key), link.sourceObject.key                         }                         td { // new reversed link                             a href: urlify(newDestinationIssue.key), newDestinationIssue.key                             i issueLinkType.getInward()                             a href: urlify(newSourceIssue.key), newSourceIssue.key                         }                     }                     if (!preview) {                         log.warn "reversing existing link '${link.sourceObject.key} ${link.getIssueLinkType().getOutward()} ${link.destinationObject.key}' "                         issueLinkManager.createIssueLink(newSourceIssue.id, newDestinationIssue.id, issueLinkType.id, sequence, loggedInUser)                         issueLinkManager.removeIssueLink(link, loggedInUser)                     }                 }             }             // reverse link direction for outwards links             if (outwardLinksShouldBeReversed) {                 def newDestinationIssue = issue                 outwardLinks.each { IssueLink link ->                     def newSourceIssue = link.destinationObject                     tr {                         td { // Original link                             a href: urlify(link.sourceObject.key), link.sourceObject.key                             i link.getIssueLinkType().getOutward()                             a href: urlify(link.destinationObject.key), link.destinationObject.key                         }                         td { // new reversed link                             a href: urlify(newSourceIssue.key), newSourceIssue.key                             i issueLinkType.getOutward()                             a href: urlify(newDestinationIssue.key), newDestinationIssue.key                         }                     }                     if (!preview) {                         issueLinkManager.createIssueLink(newSourceIssue.id, newDestinationIssue.id, issueLinkType.id, sequence, loggedInUser)                         issueLinkManager.removeIssueLink(link, loggedInUser)                         log.warn "reversing existing link '${link.sourceObject.key} ${link.getIssueLinkType().getOutward()} ${link.destinationObject.key}' "                     }                 }             }         }     }     return writer.toString() } static def urlify(issuekey) {     def baseUrl = ComponentAccessor.applicationProperties.getString(APKeys.JIRA_BASEURL)     return "${baseUrl}/browse/${issuekey}" } SearchResults<Issue> jqlSearch( String jqlString, ApplicationUser user, int index) {     try {         int pageSize = 10000;         //log.debug( "performing search with jql $jqlString" );         SearchService searchService = ComponentAccessor.getComponent(SearchService.class);         SearchService.ParseResult parseResult = searchService.parseQuery(user, jqlString);         if (parseResult.isValid()) {             SearchResults<Issue> results = searchService.search(user, parseResult.getQuery(), PagerFilter.newPageAlignedFilter(index, pageSize));             return results;         }     } catch (Exception e) {         log.warn( "Error into JQL Search with jqlString ${jqlString}" );     }     return null ; } This helps a lot and make your work easy

            +1

            @Jochen Neuhaus

            I tested your script and it works just perfect. But could you help to extend it with using JQL as source of issues?

            Thanks in advance

            ihor.zozuliak added a comment - @Jochen Neuhaus I tested your script and it works just perfect. But could you help to extend it with using JQL as source of issues? Thanks in advance

            @paul - I wrote a scriptrunner script that allows to bulk-reverse the link direction of issues for a specific link type. I used that to reverse links from CSV import:
            https://forum.library.adaptavist.com/t/reverse-issue-link-direction/112
            (the linked version requires a single issue key as input, but this can easily be extended to process a list of issue keys).

            Jochen Neuhaus added a comment - @paul - I wrote a scriptrunner script that allows to bulk-reverse the link direction of issues for a specific link type. I used that to reverse links from CSV import: https://forum.library.adaptavist.com/t/reverse-issue-link-direction/112 (the linked version requires a single issue key as input, but this can easily be extended to process a list of issue keys).

            @Paul Alexander 

            Sorry to hear about your troubles. I have had to deal with similar issue.

            My solution was to process the data in excel using vlookup to generate the csv.

            I don't know what your source data is, but I assume you have these 1562 issue links in some digital form - in a table.

            Pavol Harvanka added a comment - @Paul Alexander  Sorry to hear about your troubles. I have had to deal with similar issue. My solution was to process the data in excel using vlookup to generate the csv. I don't know what your source data is, but I assume you have these 1562 issue links in some digital form - in a table.

            Paul Alexander added a comment - - edited

            I desperately need this. I'm now left with adding 1,562 links manually. I can't even use the Bulk Edit functionality since the linkedIssues field isn't exposed for edit. Of course, folks have said, 'Well, create a CSV with the parent issues and denote the outward link relationship from there". By the time I lay down all of the data into the csv, using multiple monitors to add the right keys into every row I could have manually added the link through the UI by opening all 1,562 issues manually.

            Paul Alexander added a comment - - edited I desperately need this. I'm now left with adding 1,562 links manually. I can't even use the Bulk Edit functionality since the linkedIssues field isn't exposed for edit. Of course, folks have said, 'Well, create a CSV with the parent issues and denote the outward link relationship from there". By the time I lay down all of the data into the csv, using multiple monitors to add the right keys into every row I could have manually added the link through the UI by opening all 1,562 issues manually.

            Plase add user 'Xiangzhen.Huo@Honeywell.com' as a watcher to this issue

            naga satish kumar added a comment - Plase add user 'Xiangzhen.Huo@Honeywell.com' as a watcher to this issue

            7bdfe175ac48 No, because Data Center is still in development. All Server bugs are shared with Data Center (the converse isn't true, though).

            Piotr Janik added a comment - 7bdfe175ac48 No, because Data Center is still in development. All Server bugs are shared with Data Center (the converse isn't true, though).

              Unassigned Unassigned
              lsaw@atlassian.com Leon (Inactive)
              Votes:
              333 Vote for this issue
              Watchers:
              147 Start watching this issue

                Created:
                Updated: