StKob
StKob

Reputation: 101

How to hide parameter conditionally

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

Answers (4)

kozone
kozone

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

Stoinov
Stoinov

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.

UPDATE

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=&quot;""" +
  field + """&quot;]')[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.

Old answer follows:

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=&quot;FIRST_PARAM&quot;]')[0].closest('.jenkins-form-item').style.display='none'">
  <img src="force/failed/load/image" style="display: none;" onerror="document.querySelectorAll('input[value=&quot;SECOND_PARAM&quot;]')[0].closest('.jenkins-form-item').style.display=''">
"""
} else {
  return """
  <img src="force/failed/load/image" style="display: none;" onerror="document.querySelectorAll('input[value=&quot;FIRST_PARAM&quot;]')[0].closest('.jenkins-form-item').style.display=''">
  <img src="force/failed/load/image" style="display: none;" onerror="document.querySelectorAll('input[value=&quot;SECOND_PARAM&quot;]')[0].closest('.jenkins-form-item').style.display='none'">
"""
}

dynamically Show/Hide parameters based on selected MONITORED_PARAM

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 &quot; 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

kagen
kagen

Reputation: 154

Updated 2023/1/1

Jenkins version: 2.384

Active Choices Plug-in: 2.6.4

enter image description here

In this version, form-group has become jenkins-form-item.


My Jenkins version is 2.272, I achieved that with Active Choices plugin too.

  1. Add an Active Choices Reactive Reference Parameter named PARA_1
  2. Add an Active Choices Reactive Reference Parameter named PARA_2 that you want to hidden when PARA_1 is checked
  3. Set Referenced parameters with PARA_1

enter image description here

  1. Set Choice Type with Formatted HTML
  2. Write script

enter image description here

PARA_1 script:

return "<input name='value' type='checkbox' checked='checked'>"

enter image description here

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 enter image description here

enter image description here

Upvotes: 5

StKob
StKob

Reputation: 101

Introduction

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.

First Solution

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
}

Second Solution

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

Related Questions