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
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 must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Ah, I think I see the miscommunication now.
I am not trying to change the potential options. Rather, just the selected values. Consider:
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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")
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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 )
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
@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?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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")
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Glad I was able to help.
If you feel it was helpful, please accept the answer.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.