├── com └── bd │ └── rspeng │ └── alwaysDisplaySelectList │ ├── hideSelectList.js │ ├── masqueradeSelectList.js │ └── AlwaysDisplaySelectList.groovy ├── configurationScript.groovy └── README.md /com/bd/rspeng/alwaysDisplaySelectList/hideSelectList.js: -------------------------------------------------------------------------------- 1 | AJS.toInit(function(){ 2 | hideScriptedField() 3 | }); 4 | 5 | function hideScriptedField() { 6 | AJS.$('#rowForcustomfield_SCRIPTED_FIELD_INDEX_ID_PLACEHOLDER').hide() 7 | } 8 | -------------------------------------------------------------------------------- /configurationScript.groovy: -------------------------------------------------------------------------------- 1 | import com.bd.rspeng.alwaysDisplaySelectList.AlwaysDisplaySelectList 2 | 3 | /** 4 | * PARAMETERS 5 | */ 6 | def String customFieldName = "The name of the custom Select List field I want to always display" 7 | def int scriptedFieldIndexId = 8 | 9 | /** 10 | * LOGIC SECTION 11 | */ 12 | def scriptedFieldIndexIdAsString = ""+scriptedFieldIndexId 13 | def myScriptedSelectList = new AlwaysDisplaySelectList(scriptedFieldIndexIdAsString,customFieldName,issue) 14 | myScriptedSelectList.main() 15 | /** 16 | * END LOGIC SECTION 17 | */ -------------------------------------------------------------------------------- /com/bd/rspeng/alwaysDisplaySelectList/masqueradeSelectList.js: -------------------------------------------------------------------------------- 1 | var debug = true 2 | AJS.toInit(function(){ 3 | masqueradeScriptedFieldAsCustomField() 4 | }); 5 | 6 | function updateCustomFieldTo() { 7 | var dataObject = { 8 | "fields": { 9 | 'CUSTOM_FIELD_REST_NAME_PLACEHOLDER':{"value":getMasqueraderSelectValue()} 10 | } 11 | }; 12 | var data = JSON.stringify(dataObject) 13 | log("data for PUT: ",data) 14 | updateJIRA(data) 15 | 16 | } 17 | 18 | function getMasqueraderSelectValue() { 19 | var masqueraderSelectTag = document.getElementById('MASQUERADER_ID_PLACEHOLDER') 20 | log("masqueraderSelectTag",masqueraderSelectTag) 21 | var masqueraderSelectValue = masqueraderSelectTag.options[masqueraderSelectTag.selectedIndex].value 22 | return masqueraderSelectValue 23 | } 24 | 25 | function updateJIRA(data) { 26 | AJS.$.ajax({ 27 | type: 'PUT', 28 | contentType: 'application/json', 29 | url: 'AJAX_URL_PLACEHOLDER', 30 | data: data, 31 | dataType: 'json', 32 | success: function () { 33 | log('Update success') 34 | window.location.reload() 35 | }, 36 | error: function () { 37 | log('Update fail') 38 | } 39 | }); 40 | } 41 | 42 | function log(message,data) { 43 | if (debug) { 44 | (data == undefined) ? console.log(message) : console.log(message,data) 45 | } 46 | } 47 | 48 | function masqueradeScriptedFieldAsCustomField() { 49 | AJS.$('#rowForcustomfield_SCRIPTED_FIELD_INDEX_ID_PLACEHOLDER > div > strong').text('CUSTOM_FIELD_NAME_PLACEHOLDER:') 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is open-source out of the goodness of my company’s heart. There is no warranty or guarantee express or implied. Feel free to contact me if you have questions, but I cannot guarantee a response time. 2 | 3 | Steps to install: 4 | 5 | 1. In JIRA: install ScriptRunner plugin 6 | 2. In JIRA: create a custom Select List field 7 | 3. In JIRA: create a ScriptRunner scripted field *of a different name than the field you created in Step 2* 8 | 4. Find out the scripted field’s ID #: in the JIRA Administration > Issues screen, click Custom Fields, then click the gear next to the scripted field, then hover over the first option that appears; look in your browser’s status row (usually at the bottom of the browser) to see the ID # in the displayed URL 9 | 5. Copy this GitHub repo’s entire directory structure (except for the README.md) into your JIRA scripts folder. If you want to rearrange anything, note that you might need to modify the “package”, “import”, and “SOURCE_DIR” values. 10 | 6. Edit the configurationScript.groovy file to enter in: 11 | * The exact name of the custom Select List field 12 | * The ID # of the scripted field 13 | 7. In JIRA: configure the scripted field to use these files by going to Administration > Addons > Scripted Fields. Click the Edit link for the scripted field you’ve created. Put in the script field the /path/to/configurationScript.groovy (or D:\path\to\configurationScript.groovy). Set the output to be HTML. Click the Update button. 14 | * Note that if you click the Preview button with an issue and then change the value of the generated drop-down, the screen will refresh and your settings will be lost at this point (it will also alter the value of that issue's custom select list field). So: click Update first, then go back and Preview. 15 | 8. If all works well, add the custom Select List field and scripted field to the “Default” screen. 16 | 17 | Background: 18 | In 2004 -- see https://jira.atlassian.com/browse/JRA-2997 -- Atlassian made the decision that JIRA would not display custom fields on the issue view screen if those custom fields had no set value / were empty. The only way to force a custom field to always display on the issue view screen was to set it to required and then force a default value. 19 | 20 | One problem with setting a custom field to be required in JIRA is that it breaks Crucible / JIRA integration for custom Select List fields: errors are displayed when creating an issue in-line in Crucible and if those errors are ignored, there is a risk that JIRA might create orphan subtasks with no parent task attached. 21 | 22 | For the past 11 years, JIRA users and administrators have been clamouring for Atlassian to change this situation; so far, Atlassian has put other features as higher priority to people's chagrin even though over 150 people voted for a fix on Atlassian's own JIRA tracker. 23 | 24 | We ourselves discovered this situation at my company recently -- and it appeared no one as of yet had published a comprehensive solution. I wrote -- and then got company approval to open-source -- the above-linked mix of Groovy and JavaScript code that creates a ScriptRunner scripted field that, given a specified custom Select List field, will masquerade as that specified custom Select List field when that specified custom Select List field has no set value / is empty. 25 | 26 | The [ScriptRunner plugin for JIRA|https://marketplace.atlassian.com/plugins/com.onresolve.jira.groovy.groovyrunner/cloud/overview] is available via the Atlassian Marketplace and this code is open-source. Please try this out and suggest improvements via the GitHub system. 27 | 28 | Good luck! 29 | -------------------------------------------------------------------------------- /com/bd/rspeng/alwaysDisplaySelectList/AlwaysDisplaySelectList.groovy: -------------------------------------------------------------------------------- 1 | package com.bd.rspeng.alwaysDisplaySelectList 2 | 3 | import com.atlassian.jira.component.ComponentAccessor 4 | import com.atlassian.jira.issue.Issue 5 | import com.atlassian.jira.issue.customfields.manager.OptionsManager 6 | import com.atlassian.jira.issue.fields.CustomField 7 | 8 | import org.apache.log4j.* 9 | import groovy.util.logging.* 10 | 11 | @Log4j 12 | class AlwaysDisplaySelectList { 13 | 14 | final static SOURCE_DIR = './../../data/jira/scripts/com/bd/rspeng/alwaysDisplaySelectList' 15 | 16 | final static SCRIPTED_FIELD_INDEX_ID_PLACEHOLDER = "SCRIPTED_FIELD_INDEX_ID_PLACEHOLDER" 17 | final static AJAX_URL_PLACEHOLDER = "AJAX_URL_PLACEHOLDER" 18 | final static CUSTOM_FIELD_REST_NAME_PLACEHOLDER = "CUSTOM_FIELD_REST_NAME_PLACEHOLDER" 19 | final static MASQUERADER_ID_PLACEHOLDER = "MASQUERADER_ID_PLACEHOLDER" 20 | final static CUSTOM_FIELD_NAME_PLACEHOLDER = "CUSTOM_FIELD_NAME_PLACEHOLDER" 21 | 22 | final static OptionsManager optionsManager = ComponentAccessor.getComponent(OptionsManager.class) 23 | 24 | private String scriptedFieldIndexId = null 25 | private String customFieldName = null 26 | private Issue issue = null 27 | private CustomField customFieldObject = null 28 | private Object customFieldStringValue = null 29 | private long customFieldIndexID = 0 30 | private String customFieldRestName = null 31 | private String masqueraderID = null 32 | 33 | public AlwaysDisplaySelectList(String scriptedFieldIndexId, String customFieldName, Issue issue) { 34 | log.setLevel(org.apache.log4j.Level.INFO) 35 | this.scriptedFieldIndexId = scriptedFieldIndexId 36 | this.customFieldName = customFieldName 37 | this.issue = issue 38 | this.customFieldObject = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName(customFieldName) 39 | this.customFieldStringValue = issue.getCustomFieldValue(customFieldObject) 40 | this.customFieldIndexID = customFieldObject.idAsLong 41 | this.customFieldRestName = new StringBuilder().append("customfield_").append(customFieldIndexID).toString() 42 | this.masqueraderID = new StringBuilder().append("injectedCustomFieldMasquerader_").append(customFieldIndexID).toString() 43 | } 44 | 45 | public main() { 46 | if (customFieldStringValue != null) { 47 | generateHideSelectListJavaScript() 48 | } else { 49 | generateMasqueradeSelectList() 50 | } 51 | } 52 | 53 | private String generateMasqueradeSelectList() { 54 | def StringBuilder output = new StringBuilder() 55 | output << '' 56 | output << '' 63 | output.toString() 64 | } 65 | 66 | private String getAJAXURL() { 67 | def baseURL = ComponentAccessor.getApplicationProperties().getString("jira.baseurl") 68 | new StringBuilder().append(baseURL).append("/rest/api/2/issue/").append(issue.key).toString() 69 | } 70 | 71 | private String generateHideSelectListJavaScript() { 72 | def fileContents = new File(SOURCE_DIR + "/hideSelectList.js").getText('UTF-8') 73 | fileContents = replaceScriptedFieldIndexIdPlaceholder(fileContents) 74 | new StringBuilder().append('').toString() 75 | } 76 | 77 | private String getMasqueradeSelectListJavaScript() { 78 | def fileContents = new File(SOURCE_DIR + "/masqueradeSelectList.js").getText('UTF-8') 79 | fileContents = replaceMasqueradeSelectListPlaceholders(fileContents) 80 | fileContents 81 | } 82 | 83 | private String replaceMasqueradeSelectListPlaceholders(String fileContents) { 84 | fileContents = fileContents.replace(CUSTOM_FIELD_REST_NAME_PLACEHOLDER, customFieldRestName) 85 | fileContents = fileContents.replace(AJAX_URL_PLACEHOLDER, getAJAXURL()) 86 | fileContents = fileContents.replace(MASQUERADER_ID_PLACEHOLDER, masqueraderID) 87 | fileContents = fileContents.replace(CUSTOM_FIELD_NAME_PLACEHOLDER, customFieldName) 88 | fileContents = replaceScriptedFieldIndexIdPlaceholder(fileContents) 89 | fileContents 90 | } 91 | 92 | private String replaceScriptedFieldIndexIdPlaceholder(String fileContents) { 93 | fileContents.replace(SCRIPTED_FIELD_INDEX_ID_PLACEHOLDER, scriptedFieldIndexId) 94 | } 95 | } --------------------------------------------------------------------------------