Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Next challenges

Recent achievements

  • Global
  • Personal

Recognition

  • Give kudos
  • Received
  • Given

Leaderboard

  • Global

Trophy case

Kudos (beta program)

Kudos logo

You've been invited into the Kudos (beta program) private group. Chat with others in the program, or give feedback to Atlassian.

View group

It's not the same without you

Join the community to find out what other Atlassian users are discussing, debating and creating.

Atlassian Community Hero Image Collage

Apply Script to user editable field

Is it possible to make a 'Select List (multiple choices)' field that a user can interact with but is also controlled by a script? For example, I want a parent issue to "inherit" the selections of a field of its children if it has any, but if it does not, then allow the user be able to edit the field as normal.

I looked at Behaviors, but those appear to apply only on "Screens". I have a constraint that I also want this field to be "live" in Structure, where there is no such "Screen" on which the Behavior would apply.

Is this possible?

Thanks,

David

1 answer

1 accepted

0 votes
Answer accepted

Hi David

Unless there is some other custom field from the marketplace that supports dynamic lookup based on other issue attributes you can't achieve this with standard fields using Scriptrunner but not within the confines of what is supported by Behaviours.

The solution that comes to mind involves the Select List Conversion. But this would render the field as a regular text field while using Structures.

Hi Peter-Dave,

It's ok for Structure to render regular text. 

I'm not sure which part of Select List Conversion I should be looking at?

Do I start with a usual (non-script) MultiSelect Custom Field? I see a function 'convertToMultiSelect' - what would I need to convert TO a MutliSelect if I already have the field as a MultiSelect?

David

You have 2 choices.

1) Use the existing Multi-Select field. Continue to maintain the overall list of values (options) in the Custom Field context admin screen. In which case, you won't use the Select List Conversion, but instead, we will use the "setFieldOption()" method to dynamically show/hide some of the options (the option must first exist in the custom field config).

In structure, when attempting to do in-line edits, you will still get the full list.

2) Using Select List Conversion, you would first create a new custom field that you want to act like a multi-select. It has to be a regular text input field.
In a behaviour script, you would then either hard code the list of values you want to allow (with some logic of which values to display or not) or build a separate custom REST Endpoint script to load those values from some other data source (those values can then be stored elsewhere and managed without needed jira admin access).

In structure, when attempting to do in-line edits, you will get a text field that users can modify without validation.

Option 1 sounds great, except I don't understand where to put the code. It's not a Script Field, so there's no 'Inline script' to control the field. And it can't be a Behavior, because that doesn't get executed by Structure. I must be missing your point?inline.jpg

My point is that it is NOT possible to limit the option from multi-select from Structure.

The options I provided were to present implications given that there are no other choices.

I you go with #1, structure will always show ALL options.
If you go with #2, structure will offer the field as an editable free text

No real good options there.

Ah, I think I see the miscommunication now.

I am not trying to change the potential options. Rather, just the selected values. Consider:

  • 'Issue 1' of IssueType='Test' with a Custom Field multi-select list with options 'A', 'B', and 'C'. 'Issue 1' needs no special behavior:
    • the user can interactively selects some of the options, say 'A' and 'B', on a normal issue Screen (i.e. by clicking 'Edit')
    • Structure displays the selected options ('A' and 'B') immediately as soon as the user updates their choice from anywhere (e.g. the Edit Screen).
  • 'Issue 2', also of IssueType='Test', has 'Issue 1' as a child issue (using some kind of Link). The behavior of 'Issue 2' that I want is:
    • the user can NOT interactive select any options from the Edit Screen - they should be "locked" because they are inherited from the Child, 'Issue 1'.
    • Structure must display 'A' and 'B', just as if the user had selected them directly on the issue.

In neither case am I worried about the "Edit" behavior from within Structure. I just want a read-only display that behaves as above.

Does that clarify my goal?

David

Ah I see. 
Then I think what you should do is use an event listener to detect when multi select is changed On issue 1 and apply those same options to issue two. You can use behavior to block edits on issue 2.  

Ah, perfect - I was not aware of Listeners! Below is what I came up with. It seems to do the trick with what I asked for, but the updateValue call doesn't seem to trigger another event so this doesn't recursively apply up the tree. That is, consider:

- Issue 1

  - Issue 2

    - Issue 3

    - Issue 4

  - Issue 5

 

If I change Issue 3, I want:

- Issue 2 becomes the union of issue 3 and 4

- Issue 1 becomes the union of issue 2 and 5

 

Of course I could follow this tree programatically - but I was hoping that by manually changing Issue 3, the Listener would change Issue 2 which would trigger another Listener that would change Issue 1. Is there a way to do that?

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.customfields.option.Option

log.error(event.issue.issueTypeObject.name)
def productsCustomFieldObject = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(43005)
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

def currentIssue = event.issue as Issue

log.error("currentIssue: " + currentIssue.getKey())

def unionChildOptions(Issue issue) {
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def productsCustomFieldObject = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(43005)

def childIssues = ComponentAccessor.getIssueLinkManager().getLinkCollection(issue, currentUser).getInwardIssues("SubTeam")

def optionUnion = [] as Set<Option>

childIssues.each { childIssue ->;
log.error("Child Team: " + childIssue.getKey())

def childProductsValues = childIssue.getCustomFieldValue(productsCustomFieldObject) as List<Option>

log.error("Child Team options: " + childProductsValues.toString())

optionUnion.addAll(childProductsValues)
log.error("optionUnion: " + optionUnion.toString())
}

def allOptions = [] as List<Option>

optionUnion.each {value ->;
allOptions.add(value)
}
log.error("allOptions: " + allOptions.toString())

def currentValues = issue.getCustomFieldValue(productsCustomFieldObject) as List<Option>
productsCustomFieldObject.updateValue(null, issue, new ModifiedValue(currentValues, allOptions), new DefaultIssueChangeHolder())
}


if(currentIssue.issueTypeObject.name != "Team")
{
return;
}

log.error("Team issue found")

def parentIssues = ComponentAccessor.getIssueLinkManager().getLinkCollection(currentIssue, currentUser).getOutwardIssues("SubTeam")

if (parentIssues != null){
parentIssues.each {parentIssue ->;
if (parentIssue.getIssueType().getName() == "Team"){
log.error("Parent Team: " + parentIssue.getKey())
unionChildOptions(parentIssue)
}
}
}
else
{
log.error("No Parent Team")
}

I never use the CustomField.updateValue() method for that reason. That is a really low level api. It doesn't generate any event and doesn't show up in the issue's history and it doesn't update the index.

The next level up API is IssueManager. It will generate the history, but won't update the index. You decide whether you generate an event or not 

Here is a snipet of updating with event generated and indexing example

import com.atlassian.jira.issue.index.IssueIndexingService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.util.ImportUtils
import com.atlassian.jira.event.type.EventDispatchOption

def issueManager = ComponentAccessor.issueManager
def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issue = issueManager.getIssueObject('JSP-1922')
//using text field for simplicity
def cf= ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).find{it.name == 'Text Custom Field'}
issue.setCustomFieldValue(cf, "Test")
issueManager.updateIssue(user,issue,EventDispatchOption.ISSUE_UPDATED,false)

def indexingService = ComponentAccessor.getComponent(IssueIndexingService)
def wasIndexing = ImportUtils.isIndexIssues()
ImportUtils.setIndexIssues(true)
indexingService.reIndex(issue)
ImportUtils.setIndexIssues(wasIndexing)

The highest level API is IssueService this will take care of the indexing. It provides some proper permission validation. And you still have a choice to raise or not an event but by default, one is raised. This is the closes to what happens when you use the UI.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueInputParameters
import com.atlassian.jira.event.type.EventDispatchOption

def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issue = ComponentAccessor.issueManager.getIssueObject('JSP-1922')
def issueService = ComponentAccessor.issueService
def cf= ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).find{it.name == 'Text Custom Field'}
def params = issueService.newIssueInputParameters()
params.addCustomFieldValue(cf.id, 'Test')
def validationResuls= ComponentAccessor.issueService.validateUpdate(user,issue.id, params)
ComponentAccessor.issueService.update(user, validationResuls )

@Peter-Dave Sheehan thanks for the explanation.

The only signature I see is:

IssueInputParameters addCustomFieldValue(Long customFieldId,
                                         String... values)

 but my 'values' is a List<Option>, it seems like I need to do this (this seems really awkward?)?

def params = ComponentAccessor.issueService.newIssueInputParameters()

log.error("building allOptionsAsStrings")
def allOptionsAsStrings = [] as List<String>
allOptions.each { option ->;
allOptionsAsStrings.add(option.getValue())
}
log.error("done building allOptionsAsStrings")
params.addCustomFieldValue(productsCustomFieldObject.id, allOptionsAsStrings.toArray(new String[allOptionsAsStrings.size()]))
log.error("done addCustomFieldValue")
def validationResults = ComponentAccessor.issueService.validateUpdate(currentUser, issue.id, params)
log.error("done validateUpdate")
ComponentAccessor.issueService.update(currentUser, validationResults )
log.error("end unionChildOptions")

Unfortunately, the Listener fails. The log stops at "done addCustomFieldValue" - I never see "done validateUpdate".

Any thoughts?

Frankly I'm more likely to use the issueManager approach.

So I never really experimented with the issueService.

A bit of research showed that this is the correct way to add multiple values to an issueInputParameter:

params.addCustomFieldValue(cf.id, '10405', '10406')

Where 10405 and 10406 are optionId for the options to add.

After a bit more trial and error, and I'm not clear why the following works, the following can be used to  add multiple options from an array

def optionIdArray = allOptions.collect{it.optionId.toString()}.toArray(new String[allOptions.size()])
params.addCustomFieldValue(cf.id, optionIdArray)
def validationResults= ComponentAccessor.issueService.validateUpdate(user,issue.id, params)
if(validationResults.isValid()){
ComponentAccessor.issueService.update(user, validationResults )
} else {
log.error validationResults.errorCollection
}

That's more compact, but still doesn't work :(

The end of the function looks like:

 def params = ComponentAccessor.issueService.newIssueInputParameters()

def optionIdArray = allOptions.collect{it.optionId.toString()}.toArray(new String[allOptions.size()])
log.error("optionIdArray: " + optionIdArray.toString())
params.addCustomFieldValue(productsCustomFieldObject.id, optionIdArray)
log.error("done addCustomFieldValue")
def validationResults = ComponentAccessor.issueService.validateUpdate(currentUser, issue.id, params)
log.error("done validateUpdate")
if(validationResults.isValid()){
ComponentAccessor.issueService.update(currentUser, validationResults)
} else {
log.error validationResults.errorCollection
}

log.error("end unionChildOptions")

and the end of the log shows:

2020-07-25 21:38:01,355 ERROR [runner.ScriptRunnerImpl]: done building allOptionsAsStrings
2020-07-25 21:38:01,357 ERROR [runner.ScriptRunnerImpl]: optionIdArray: [196963, 196964]
2020-07-25 21:38:01,357 ERROR [runner.ScriptRunnerImpl]: done addCustomFieldValue

(i.e. the validateUpdate call never returns).

I'll try the issueManager approach now.

David

Yeah, that's a strange one.

Where are you looking at the log? Just in the scriptrunner execution history or in the jira-home/log/atlassian-jira.log file directly?

Sometimes, I've seen instances where scriptrunner loses track of threads for logging purposes.

Here is a full screenshot of my working example in the console:

2020-07-25 15_01_26-Script Console - https___projects-qsmtest.qad.com_plugins_servlet_scriptrunner_a.png

It works! Thanks a ton for your help.

 

Here is the whole script for posterity:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.customfields.option.Option
import com.atlassian.jira.issue.IssueInputParameters
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.index.IssueIndexingService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.util.ImportUtils
import com.atlassian.jira.event.type.EventDispatchOption

log.error(event.issue.issueTypeObject.name)
def productsCustomFieldObject = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(43005)
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

def currentIssue = event.issue as Issue

log.error("currentIssue: " + currentIssue.getKey())

def unionChildOptions(Issue issue) {
log.error("start unionChildOptions")
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def productsCustomFieldObject = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(43005)

def childIssues = ComponentAccessor.getIssueLinkManager().getLinkCollection(issue, currentUser).getInwardIssues("SubTeam")

def optionUnion = [] as Set<Option>

childIssues.each { childIssue ->;
log.error("Child Team: " + childIssue.getKey())

def childProductsValues = childIssue.getCustomFieldValue(productsCustomFieldObject) as List<Option>

log.error("Child Team options: " + childProductsValues.toString())

optionUnion.addAll(childProductsValues)
log.error("optionUnion: " + optionUnion.toString())
}

def allOptions = [] as List<Option>
optionUnion.each {value ->;
allOptions.add(value)
}

//using text field for simplicity
def issueObject = ComponentAccessor.issueManager.getIssueObject(issue.getKey())

issueObject.setCustomFieldValue(productsCustomFieldObject, allOptions)
ComponentAccessor.issueManager.updateIssue(currentUser, issueObject, EventDispatchOption.ISSUE_UPDATED, false)

def indexingService = ComponentAccessor.getComponent(IssueIndexingService)
def wasIndexing = ImportUtils.isIndexIssues()
ImportUtils.setIndexIssues(true)
indexingService.reIndex(issue)
ImportUtils.setIndexIssues(wasIndexing)

}


if(currentIssue.issueTypeObject.name != "Team")
{
return;
}

log.error("Team issue found")

def parentIssues = ComponentAccessor.getIssueLinkManager().getLinkCollection(currentIssue, currentUser).getOutwardIssues("SubTeam")

if (parentIssues != null){
parentIssues.each {parentIssue ->;
if (parentIssue.getIssueType().getName() == "Team"){
log.error("Parent Team: " + parentIssue.getKey())
unionChildOptions(parentIssue)
}
}
}
else
{
log.error("No Parent Team")
}

Glad I was able to help.

If you feel it was helpful, please accept the answer.

Suggest an answer

Log in or Sign up to answer
TAGS
Community showcase
Published in Jira

Jira Cloud Performance Improvements

Hello everyone, I am a product manager in the Jira Cloud team focused on making sure our customers have a delightful experience using our products. Towards that goal, one of the areas which is extr...

184 views 2 9
Read article

Community Events

Connect with like-minded Atlassian users at free events near you!

Find an event

Connect with like-minded Atlassian users at free events near you!

Unfortunately there are no Community Events near you at the moment.

Host an event

You're one step closer to meeting fellow Atlassian users at your local event. Learn more about Community Events

Events near you