Reputation: 101
I was able to make inputs appear and dissappear with active choices plugin. But name of the parameter isn't hidden dynamically and I don't know how to achieve that.
Upvotes: 6
Views: 13855
Reputation: 139
I found these answers to be a little confusing. After a LOT of trial and error, I was able to get it working. It can only use String, Choice, or Boolean parameters as the monitor parameter, and it can only hide one parameter at a time, but I have wrapped the entire thing in a function which you can put in the job properties and call as many times as needed:
The wrapper function, I stored in a shared library vars file called sharedParameters.groovy
, and I call it in my Jenkinsfiles like this:
sharedParameters.conditionalParamHider(
paramToHide: 'MY_CONDITIONAL_PARAM',
monitorParam: 'PARAM_WHOSE_VALUE_IS_CHECKED',
hideIfThisValue: 'Inputting or selecting this value in monitorParam will cause the param to be hidden.',
),
The conditionalParamHider
is defined like so:
def conditionalParamHider(Map args) {
// Required args:
// paramToHide - this is the param which we will be able to hide conditionally based on the monitor param
// monitorParam - this is the name of the param whose value will determine whether to hide a different param.
// hideIfThisValue - this is the value of the param whose value will determine whether to hide a different param.
String hiderScript = $/
return """
<style>
/* Initially hide in case it's already loaded */
input[name="name"][value="${args.paramToHide}"] { display: none; }
</style>
<script>
function update${args.paramToHide}() {
var monitorNameField = document.querySelector('input[name="name"][value="${args.monitorParam}"]');
var field = document.querySelector('input[name="name"][value="${args.paramToHide}"]');
if (monitorNameField && field) {
var tbody = field.closest('tbody');
// Get the value field (works for text inputs, dropdowns, and checkboxes)
var monitorValueField = monitorNameField.closest('div[name="parameter"]').querySelector('input[name="value"], select[name="value"], input[type="checkbox"]');
if (tbody && monitorValueField) {
var monitorValue = "";
// Handle checkboxes separately
if (monitorValueField.type === "checkbox") {
monitorValue = monitorValueField.checked ? "yes" : "no"; // Convert checkbox state to yes/no
} else {
monitorValue = monitorValueField.value.trim().toLowerCase();
}
console.log("${args.monitorParam} detected value:", monitorValue);
if (monitorValue === "${args.hideIfThisValue}".toLowerCase()) {
tbody.style.display = 'none';
console.log("${args.paramToHide} is hidden because ${args.monitorParam} is '${args.hideIfThisValue}'.");
} else {
tbody.style.display = '';
console.log("${args.paramToHide} is visible.");
}
}
} else {
console.log("${args.monitorParam} or ${args.paramToHide} parameter not found.");
}
}
// Run immediately in case the element is already present
update${args.paramToHide}();
// Observe for dynamic changes (Jenkins UI updates)
var observer = new MutationObserver(function(mutations, obs) {
update${args.paramToHide}();
});
observer.observe(document.body, { childList: true, subtree: true });
// Listen for changes to the monitor parameter input, dropdown, or checkbox
document.addEventListener('change', function(event) {
if (event.target && (event.target.name === "value" || event.target.tagName === "SELECT" || event.target.type === "checkbox")) {
update${args.paramToHide}();
}
});
</script>
"""
/$.stripIndent().trim()
return activeChoices.hiddenScriptParam(
name: "${args.paramToHide.toUpperCase()}_HIDER",
script: hiderScript,
referencedParameters: "${args.monitorParam},${args.paramToHide}",
choiceType: 'ET_FORMATTED_HIDDEN_HTML',
)
}
This in turn relies on another vars file, activeChoices.groovy
:
def setDefaultArgs(Map args) {
args.name = args.get('name', '')
args.description = args.get('description', '')
args.referencedParameters = args.get('referencedParameters', '')
args.sandbox = args.get('sandbox', false)
args.filterable = args.get('filterable', false)
args.filterLength = args.get('filterLength', 1)
args.omitValueField = args.get('omitValueField', true)
return args
}
def reactiveReferenceParam(Map args) {
// Required args:
// name
// script
// referencedParameters
// choiceType
// Optional args, with defaults:
setDefaultArgs(args)
[$class: 'DynamicReferenceParameter',
choiceType: args.choiceType,
referencedParameters: args.referencedParameters,
omitValueField: args.omitValueField,
description: args.description,
name: args.name,
randomName: 'choice-parameter-245311171714082',
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: args.sandbox,
script: ""
],
script: [
classpath: [],
sandbox: args.sandbox,
script: args.script.stripIndent().trim()
]
]
]
}
def hiddenScriptParam(Map args) {
// Required args:
// script
// Optional args, with defaults:
setDefaultArgs(args)
return reactiveReferenceParam(
name: args.name,
script: args.script,
referencedParameters: args.referencedParameters,
choiceType: 'ET_FORMATTED_HIDDEN_HTML',
)
}
If this is all too much for you, you can just use an inline parameter like this (I have NOT tested it like this.):
[$class: 'DynamicReferenceParameter',
choiceType: 'ET_FORMATTED_HIDDEN_HTML',
referencedParameters: 'MONITOR_PARAM,PARAM_TO_HIDE',
omitValueField: true,
description: args.description,
name: 'PARAM_HIDER',
randomName: 'choice-parameter-245311171714082',
script: [
$class: 'GroovyScript',
script: [
classpath: [],
sandbox: false,
script: $/
return """
<style>
/* Initially hide in case it's already loaded */
input[name="name"][value="PARAM_TO_HIDE"] { display: none; }
</style>
<script>
function updatePARAM_TO_HIDE() {
var monitorNameField = document.querySelector('input[name="name"][value="PARAM_TO_HIDE"]');
var field = document.querySelector('input[name="name"][value="PARAM_TO_HIDE"]');
if (monitorNameField && field) {
var tbody = field.closest('tbody');
// Get the value field (works for text inputs, dropdowns, and checkboxes)
var monitorValueField = monitorNameField.closest('div[name="parameter"]').querySelector('input[name="value"], select[name="value"], input[type="checkbox"]');
if (tbody && monitorValueField) {
var monitorValue = "";
// Handle checkboxes separately
if (monitorValueField.type === "checkbox") {
monitorValue = monitorValueField.checked ? "yes" : "no"; // Convert checkbox state to yes/no
} else {
monitorValue = monitorValueField.value.trim().toLowerCase();
}
console.log("${args.monitorParam} detected value:", monitorValue);
if (monitorValue === "<THIS IS THE VALUE TO HIDE BASED UPON>".toLowerCase()) {
tbody.style.display = 'none';
console.log("PARAM_TO_HIDE is hidden because MONITOR_PARAM is '<THIS IS THE VALUE TO HIDE BASED UPON>'.");
} else {
tbody.style.display = '';
console.log("PARAM_TO_HIDE is visible.");
}
}
} else {
console.log("MONITOR_PARAM or PARAM_TO_HIDE parameter not found.");
}
}
// Run immediately in case the element is already present
updatePARAM_TO_HIDE();
// Observe for dynamic changes (Jenkins UI updates)
var observer = new MutationObserver(function(mutations, obs) {
updatePARAM_TO_HIDE();
});
observer.observe(document.body, { childList: true, subtree: true });
// Listen for changes to the monitor parameter input, dropdown, or checkbox
document.addEventListener('change', function(event) {
if (event.target && (event.target.name === "value" || event.target.tagName === "SELECT" || event.target.type === "checkbox")) {
updatePARAM_TO_HIDE();
}
});
</script>
"""
/$.stripIndent().trim()
]
]
]
Upvotes: 1
Reputation: 792
There is another way to handle this which is more general and can be used to dynamically show/hide any property field and not only active choices plugin ones. It builds on top of the answer of @kagen.
You create your parameters as usual using whatever plugins you need to. Once they are all laid out, you add just one Active Choices Reactive Reference Parameter
in your job. Give it a descriptive name like DYNAMIC_PROPERTIES_MANAGER
and maybe add description for future reference. For the Choice Type
select Formatted Hidden HTML
and for Referenced parameters
list all parameters that you want to be monitored.
After I end up having a more complex logic for showing and hiding large number of fields. The multiple returns quickly grew and became hard to manage reliably, so I extracted the unique info in a map and looped through it based on the MONITORED_PARAM
:
// Map defining which fields to hide based on selected MONITORED_PARAM value
def map = [
// Empry value displays all fields
"": [],
"Value 1": ["FIRST_PARAM"],
// Used for options not explicitly defined
"Default": ["SECOND_PARAM", "THIRD_PARAM", "FOURTH_PARAM"]]
fields_to_hide = map["Default"]
if (map.containsKey(MONITORED_PARAM)) {
fields_to_hide = map[MONITORED_PARAM]
}
def html = ""
for(field in fields_to_hide) {
html = html +
"""<img src="force/failed/load/image" style="display: none;" onerror="document.querySelectorAll('input[value="""" +
field + """"]')[0].closest('.jenkins-form-item').style.display=''">\n"""
}
return html
This script assumes you have multiple values that will present the same set of fields and they are listed under "Default"
map value, but this can be any non-matching name. If there is a match - another set of fields will be hidden.
If you end up listing all your values in the map, you can simplify even further by removing the whole if
section ad replacing fields_to_hide
in the for
loop with map[MONITORED_PARAM]
directly.
Once this is done, you need to add the following Groovy:
if ( MONITORED_PARAM == "Value" ) {
return """
<img src="force/failed/load/image" style="display: none;" onerror="document.querySelectorAll('input[value="FIRST_PARAM"]')[0].closest('.jenkins-form-item').style.display='none'">
<img src="force/failed/load/image" style="display: none;" onerror="document.querySelectorAll('input[value="SECOND_PARAM"]')[0].closest('.jenkins-form-item').style.display=''">
"""
} else {
return """
<img src="force/failed/load/image" style="display: none;" onerror="document.querySelectorAll('input[value="FIRST_PARAM"]')[0].closest('.jenkins-form-item').style.display=''">
<img src="force/failed/load/image" style="display: none;" onerror="document.querySelectorAll('input[value="SECOND_PARAM"]')[0].closest('.jenkins-form-item').style.display='none'">
"""
}
This will use MONITORED_PARAM
to dynamically show and hide FIRST_PARAM
and SECOND_PARAM
based on its value. The end result will be that the page will switch between the two params when you enter/remove Value
from the monitored parameter.
You can have complex logic with multiple values and multiple parameters that dynamically show/hide based on different combinations.
One main thing to note, is that the onerror
value uses more than 2 levels of quotes, and requires the use of "
after the second level to properly work.
This solution depends on the fact, that Jenkins adds hidden inputs for each param with the name as value, which allows the querySelectorAll
to work as described.
This has been tested on Jenkins 2.456 with Active Choices 2.8.3
Upvotes: 0
Reputation: 154
Updated 2023/1/1
Jenkins version: 2.384
Active Choices Plug-in: 2.6.4
In this version, form-group
has become jenkins-form-item
.
My Jenkins version is 2.272, I achieved that with Active Choices plugin too.
Active Choices Reactive Reference Parameter
named PARA_1
Active Choices Reactive Reference Parameter
named PARA_2
that you want to hidden when PARA_1
is checkedReferenced parameters
with PARA_1
Choice Type
with Formatted HTML
PARA_1
script:
return "<input name='value' type='checkbox' checked='checked'>"
PARA_2
script:
if (PARA_1) {
return """
<input name="value" id="PARA_2" value="PARA_2">
<img src="force/failed/load/image" style="display: none;" onerror='document.getElementById("PARA_2").closest(".jenkins-form-item").style.display=""'>
"""
} else {
return """
<input name="value" id="PARA_2" value="PARA_2">
<img src="force/failed/load/image" style="display: none;" onerror='document.getElementById("PARA_2").closest(".jenkins-form-item").style.display="none"'>
"""
}
The form-group
is class name that find by Jenkins parameters HTML
6.Finally Build with Parameters
Upvotes: 5
Reputation: 101
I wanted to show parameters when checkbox was true and hide it when false. I achieved that with Active Choices plugin. Unfortunately if you give name to parameter, this name won't hide. So I came up with two solutions.
This solution has a little flaw. Because postion is set to "absolute" everything is ok as long as window size doesnt change. This code should be run as groovy script in "Active Choices Reactive Reference Parameter" and reference PARAM_1. Parameter should return "Formatted HTML".
if(PARAM_1) {
def FullHTML = """
<label style="left: 19.9%; position: absolute;">input</label>
<input name='input_field' type='text'>
"""
return FullHTML
}
This solution uses jquery to modify "setting-name" element. This code should be run as groovy script in "Active Choices Reactive Reference Parameter" and reference PARAM_1. Parameter should return "Formatted Hidden HTML".
if(PARAM_1) {
def FullHTML = """
<input id='input_field' name='input_field' type='text'>
"""
return FullHTML
}
Add one more "Active Choices Reactive Reference Parameter" which will containt this code. Again run as groovy script and return "Formatted Hidden HTML".
EDIT
I've modified this code to hide empty space when element isn't shown on the page. Because Jenkins puts it into table each row was taking space even if it was empty.
def FullHTML = """
<script>/*
* arrive.js
* v2.4.1
* https://github.com/uzairfarooq/arrive
* MIT licensed
*
* Copyright (c) 2014-2017 Uzair Farooq
*/
var Arrive=function(e,t,n){"use strict";function r(e,t,n){l.addMethod(t,n,e.unbindEvent),l.addMethod(t,n,e.unbindEventWithSelectorOrCallback),l.addMethod(t,n,e.unbindEventWithSelectorAndCallback)}function i(e){e.arrive=f.bindEvent,r(f,e,"unbindArrive"),e.leave=d.bindEvent,r(d,e,"unbindLeave")}if(e.MutationObserver&&"undefined"!=typeof HTMLElement){var o=0,l=function(){var t=HTMLElement.prototype.matches||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector;return{matchesSelector:function(e,n){return e instanceof HTMLElement&&t.call(e,n)},addMethod:function(e,t,r){var i=e[t];e[t]=function(){return r.length==arguments.length?r.apply(this,arguments):"function"==typeof i?i.apply(this,arguments):n}},callCallbacks:function(e,t){t&&t.options.onceOnly&&1==t.firedElems.length&&(e=[e[0]]);for(var n,r=0;n=e[r];r++)n&&n.callback&&n.callback.call(n.elem,n.elem);t&&t.options.onceOnly&&1==t.firedElems.length&&t.me.unbindEventWithSelectorAndCallback.call(t.target,t.selector,t.callback)},checkChildNodesRecursively:function(e,t,n,r){for(var i,o=0;i=e[o];o++)n(i,t,r)&&r.push({callback:t.callback,elem:i}),i.childNodes.length>0&&l.checkChildNodesRecursively(i.childNodes,t,n,r)},mergeArrays:function(e,t){var n,r={};for(n in e)e.hasOwnProperty(n)&&(r[n]=e[n]);for(n in t)t.hasOwnProperty(n)&&(r[n]=t[n]);return r},toElementsArray:function(t){return n===t||"number"==typeof t.length&&t!==e||(t=[t]),t}}}(),c=function(){var e=function(){this._eventsBucket=[],this._beforeAdding=null,this._beforeRemoving=null};return e.prototype.addEvent=function(e,t,n,r){var i={target:e,selector:t,options:n,callback:r,firedElems:[]};return this._beforeAdding&&this._beforeAdding(i),this._eventsBucket.push(i),i},e.prototype.removeEvent=function(e){for(var t,n=this._eventsBucket.length-1;t=this._eventsBucket[n];n--)if(e(t)){this._beforeRemoving&&this._beforeRemoving(t);var r=this._eventsBucket.splice(n,1);r&&r.length&&(r[0].callback=null)}},e.prototype.beforeAdding=function(e){this._beforeAdding=e},e.prototype.beforeRemoving=function(e){this._beforeRemoving=e},e}(),a=function(t,r){var i=new c,o=this,a={fireOnAttributesModification:!1};return i.beforeAdding(function(n){var i,l=n.target;(l===e.document||l===e)&&(l=document.getElementsByTagName("html")[0]),i=new MutationObserver(function(e){r.call(this,e,n)});var c=t(n.options);i.observe(l,c),n.observer=i,n.me=o}),i.beforeRemoving(function(e){e.observer.disconnect()}),this.bindEvent=function(e,t,n){t=l.mergeArrays(a,t);for(var r=l.toElementsArray(this),o=0;o<r.length;o++)i.addEvent(r[o],e,t,n)},this.unbindEvent=function(){var e=l.toElementsArray(this);i.removeEvent(function(t){for(var r=0;r<e.length;r++)if(this===n||t.target===e[r])return!0;return!1})},this.unbindEventWithSelectorOrCallback=function(e){var t,r=l.toElementsArray(this),o=e;t="function"==typeof e?function(e){for(var t=0;t<r.length;t++)if((this===n||e.target===r[t])&&e.callback===o)return!0;return!1}:function(t){for(var i=0;i<r.length;i++)if((this===n||t.target===r[i])&&t.selector===e)return!0;return!1},i.removeEvent(t)},this.unbindEventWithSelectorAndCallback=function(e,t){var r=l.toElementsArray(this);i.removeEvent(function(i){for(var o=0;o<r.length;o++)if((this===n||i.target===r[o])&&i.selector===e&&i.callback===t)return!0;return!1})},this},s=function(){function e(e){var t={attributes:!1,childList:!0,subtree:!0};return e.fireOnAttributesModification&&(t.attributes=!0),t}function t(e,t){e.forEach(function(e){var n=e.addedNodes,i=e.target,o=[];null!==n&&n.length>0?l.checkChildNodesRecursively(n,t,r,o):"attributes"===e.type&&r(i,t,o)&&o.push({callback:t.callback,elem:i}),l.callCallbacks(o,t)})}function r(e,t){return l.matchesSelector(e,t.selector)&&(e._id===n&&(e._id=o++),-1==t.firedElems.indexOf(e._id))?(t.firedElems.push(e._id),!0):!1}var i={fireOnAttributesModification:!1,onceOnly:!1,existing:!1};f=new a(e,t);var c=f.bindEvent;return f.bindEvent=function(e,t,r){n===r?(r=t,t=i):t=l.mergeArrays(i,t);var o=l.toElementsArray(this);if(t.existing){for(var a=[],s=0;s<o.length;s++)for(var u=o[s].querySelectorAll(e),f=0;f<u.length;f++)a.push({callback:r,elem:u[f]});if(t.onceOnly&&a.length)return r.call(a[0].elem,a[0].elem);setTimeout(l.callCallbacks,1,a)}c.call(this,e,t,r)},f},u=function(){function e(){var e={childList:!0,subtree:!0};return e}function t(e,t){e.forEach(function(e){var n=e.removedNodes,i=[];null!==n&&n.length>0&&l.checkChildNodesRecursively(n,t,r,i),l.callCallbacks(i,t)})}function r(e,t){return l.matchesSelector(e,t.selector)}var i={};d=new a(e,t);var o=d.bindEvent;return d.bindEvent=function(e,t,r){n===r?(r=t,t=i):t=l.mergeArrays(i,t),o.call(this,e,t,r)},d},f=new s,d=new u;t&&i(t.fn),i(HTMLElement.prototype),i(NodeList.prototype),i(HTMLCollection.prototype),i(HTMLDocument.prototype),i(Window.prototype);var h={};return r(f,h,"unbindAllArrive"),r(d,h,"unbindAllLeave"),h}}(window,"undefined"==typeof jQuery?null:jQuery,void 0);
</script>
<script type='text/javascript'>
var nameElement;
function showText(textToShow, elementToEdit){
jQuery(elementToEdit).text(textToShow);
}
function hideElement(elementToHide) {
jQuery(elementToHide).css({"visibility" : "hidden", "position" : "absolute"});
}
function showElement(elementToShow) {
jQuery(elementToShow).css({"visibility" : "", "position" : ""});
}
jQuery(document).arrive("#input_field", function() {
nameElement = jQuery('#input_field').parent().parent().parent().parent().find(".setting-name");
showText("input_field", nameElement );
showElement(nameElement.parent().parent())
});
jQuery(document).leave("#input_field", function() {
showText("", nameElement);
hideElement(nameElement.parent().parent());
});
</script>
"""
return FullHTML
Upvotes: 2