Maik Skoddow
Tera Patron

find_real_file.png

A field of type "List" is nothing else than an extended form of a reference field, which can store more than one reference. They are used in many ways, and the "Watch list" for task inherited tables is certainly the most prominent example. But they also have some disadvantages. For example, it is not possible to group by these fields, which makes sense since for each record the list can have different numbers of elements. For reporting purposes, therefore, many users would like to have at least interactive filters on a dashboard that can be used to reduce a list based on the selected elements.

OOTB ServiceNow does not offer interactive filters for list type fields. However, there are some workarounds I will introduce in this article.

 

        
Table of Rules

  

Approach 1: Normalize List Values into a Custom Table

 

That approach introduces a custom table which holds single relations between the record and all list values. The normalization is done with the help of an additional Business Rule and the initial load is performed by a background script.

 

This approach has its charm, because it is generic and allows not only interactive filters, but also all kinds of reports. But on the other hand, it requires at least a custom table and some implementation efforts.

If you are interested in that approach, please find the complete solution on page Reporting on Lists – Part 3/3 on Conquer Challenging Reports by Leveraging the Now Platform which was written by Performance Analytics & Reporting guru Adam Stout.

 

In the following two chapters, I will describe my own solution proposals.

 

 

Approach 2: Leveraging Cascading Filters

 

After a while of trial and error I found out that Cascading Filters can be used to filter list type fields and the result looks like as follows:

find_real_file.png

 

Although no code is needed, this approach has a few downsides:

  • You can select only one user. Unfortunately, I couldn't figure out whether it is possible to configure a multi-selection.
  • After choosing a user, you have to click on the "Apply" button explicitly to apply the selection.

 

To configure such a filter, proceed as follows:

find_real_file.png  
  1. Open dashboard properties 
  2. Select plus sign to add a widget
  3. Select "Interactive Filters"
  4. Select "Cascading Filter"
      
find_real_file.png  
  1. Select "#New Cascading Filter"
      
find_real_file.png  
  1. Click on the "Add" button
  2. Click on the link "Click here to configure this reusable Widget" 
      
find_real_file.png  
  1. Enter a name. This value will be displayed as the widget title. 
  2. To make the filter work also on reports with child tables, this checkbox has to be set.
  3. Then save the record. Do not click on "Update"!
      
find_real_file.png  
  1.  On related list "Cascading Filter" click on "New"
      
find_real_file.png  
  1. Enter a name. That value is displayed right above the selection within the widget.
  2. Select table "User"
  3. Select "Name" as display field.
  4. Optionally, a condition can be configured to limit the set of selectable users.
  5. Then save the record. Do not click on "Update"!
      
 find_real_file.png  
  1.  At Related List "Target Tables" click on "New"
      
find_real_file.png  
  1. Select "task" as "Target table". This way the filter is more generic and can be used for all tables which extend the task table.
  2. Select "Watch list" at "Field"
  3. Submit the form.
      
find_real_file.png  

After returning to the dashboard, you should find there the previously configured widget.

  1. In the upper right corner of the widget, click on the gear icon.
      
find_real_file.png  
  1. Make sure that the checkbox "Act as interactive filer" is set.
  2. Modify the widget properties as needed.
      
find_real_file.png  

 After adding a report on a task extended table of your choice to the dashboard, open the widget properties.

  1. Select "Follow interactive filter"
  2. By selecting "Show when following filter" a filter icon right beside the widget title is displayed.

 

 

 

Approach 3: Create a Custom Interactive Filter

 

To have a better user experience, you can build a custom filter and adapt it according to your needs. The following Custom Filter works without the "Apply" button, and you can also select multiple users:

find_real_file.png

 

This pro-code approach requires your knowledge in Jelly, JavaScript, HTML and CSS and this article cannot provide an introduction to these technologies. However, in my Knowledge Sources To Go I have collected many links to helpful resources which will support you to get started with the respective topics.

 

Furthermore, you need to understand the nature of Custom Filters. In the last chapter "Resources" I have provided a link list regarding Custom Filters, and additionally I will go into more detail on individual sections of the source code.

 

Please note that my Custom Interactive Filter has the following restrictions:

  • It can be placed only once on a dashboard.
  • It only filters the watch_list field of task records.
  • It can cause performance issues if your sys_user table has many thousand entries.
  • If you select more than one user, the query criteria will be combined with an OR operator, but you could modify the code to have a AND behavior.

 

To build such a Custom Interactive Filter follow the instructions below:

 

find_real_file.png  
  1. Open dashboard properties
  2. Select plus sign to add a widget
  3. Select "Content Blocks"
  4. Select "*New Dynamic Content"
      
find_real_file.png  
  1. Click on the "Add" button
  2. Click on the link "Click here" 
      
find_real_file.png  
  1. Enter a name. This value will be displayed as the widget title
  2. Insert the Jelly-Code (see chapter "Complete Jell Code" right next to that table) and save.
      
find_real_file.png  

After returning to the dashboard, you should find there the previously configured widget.

  1. In the upper right corner of the widget, click on the gear icon.
    

 

find_real_file.png  
  1. Make sure that the checkbox "Act as interactive filer" is set.
  2. Modify the widget properties as needed.

 

 

Complete Jelly Code:

 

 

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<script>  
  var arrItemSysId            = [];
  var arrItemName             = [];
  var dashboardMessageHandler = new DashboardMessageHandler(
    "custom_watch_list_filter", 
    function() {
      resetCustomWatchListFilter()
    });
  
  
  // transform HTML selectbox to ServiceNow selection
  $j(document).ready(function() {
    $j("#myCustomInteractiveFilterChoice").select2();
  });  
 
  function rebuildCustomWatchListFilterUsers() {
    var arrOut = [];

    for (var numItemIndex = 0; numItemIndex &lt; arrItemSysId.length; numItemIndex++) {
      arrOut.push('&lt;li class="select2-search-choice"&gt;');
      arrOut.push('&lt;div&gt;');
      arrOut.push(arrItemName[numItemIndex]);
      arrOut.push('&lt;/div&gt;');
      arrOut.push('&lt;a href="#" onClick="removeCustomWatchListFilterItem(\'')
      arrOut.push(arrItemSysId[numItemIndex])
      arrOut.push('\');return(false)" role="button" class="select2-search-choice-close"&gt;&lt;/a&gt;');
      arrOut.push('&lt;/li&gt;');
    }

    $j("#myCustomInteractiveFilterSelectedItems ul:first").html(arrOut.join(''));
    $j('#myCustomInteractiveFilter span.select2-chosen').text(arrItemSysId.length == 0 ? 'ALL' : '');
  }
  
  function publishCustomWatchListFilter() {  
    var objFilter = {
      id     : 'custom_watch_list_filter',
      table  : 'task',
      filter : arrItemSysId.length > 0 ? 'watch_listDYNAMIC' + arrItemSysId : ''
    };
  
    SNC.canvas.interactiveFilters.setDefaultValue({
      id      : objFilter.id,
      filters : [objFilter]
    }, false);
  
    if (arrItemSysId.length > 0) {
      dashboardMessageHandler.publishFilter(objFilter.table, objFilter.filter);
    }
    else {
      dashboardMessageHandler.removeFilter();
    }
  }  
  
  function removeCustomWatchListFilterItem(strSysId) {
    if (typeof strSysId == 'string') {
      var numArrayIndex = arrItemSysId.indexOf(strSysId);
  
      if (numArrayIndex != -1) {
        arrItemSysId.splice(numArrayIndex, 1);
        arrItemName.splice(numArrayIndex, 1);
        rebuildCustomWatchListFilterUsers();
        publishCustomWatchListFilter();
      }
    }
  }
  
  function resetCustomWatchListFilter() {
    arrItemSysId = [];
    arrItemName  = [];
  
    rebuildCustomWatchListFilterUsers();
    publishCustomWatchListFilter();
  }
    
  function addCustomWatchListFilterItem(){
    var strSelectedValue = $j('#myCustomInteractiveFilterChoice option:selected').val();
    var strSelectedLabel = $j('#myCustomInteractiveFilterChoice option:selected').text();

    if (strSelectedValue == "all") {
      resetCustomWatchListFilter();
    }
    else {
      if (!arrItemSysId.includes(strSelectedValue)) {
        arrItemSysId.push(strSelectedValue);
        arrItemName.push(strSelectedLabel);
      }
  
      rebuildCustomWatchListFilterUsers();
      publishCustomWatchListFilter();  
    }
  }
</script>
<g:evaluate>
var arrUserList = [];
var grUser      = new GlideRecord('sys_user');

grUser.orderBy('name')
grUser.query();

while(grUser.next()) {
  arrUserList.push({
    name:  grUser.getValue('name'),
    sysId: grUser.getUniqueValue()
  });
}
</g:evaluate>   
<div class="widget-content" style="padding:10px" id='myCustomInteractiveFilter'>
<select style="width:100%;" id="myCustomInteractiveFilterChoice" class="select2-search" onchange="addCustomWatchListFilterItem();">
<div class="form-horizontal container-fluid">
  <option value="all">ALL</option>
  <j:forEach var="jvar_type" items="${arrUserList}">
    <g:evaluate var="jvar_type" jelly="true">
      var strName  = jelly.jvar_type.name;
      var strSysId = jelly.jvar_type.sysId;
    </g:evaluate>
    <option value="${strSysId}" label="${strName}">${strName}</option>
  </j:forEach>
</div>
</select>
<div id="myCustomInteractiveFilterSelectedItems" class="select2-container select2-container-multi interactive-filter__widget-content form_control" style="width: 100%;">
<ul class="select2-choices">
</ul>
</div>
</div>
</j:jelly>

 

 

 

Some aspects explained in more detail

 

var arrItemSysId = [];
var arrItemName  = [];
  These two arrays store the Sys IDs and the labels of the selected users.
   

 

var dashboardMessageHandler = 
  new DashboardMessageHandler(
    "custom_watch_list_filter", 
    function() {
      resetCustomWatchListFilter()
    });
 

Instantiation of the message handler for broadcasting the filters to all widgets on a dashboard.

The first parameter is a unique ID for the widget the second parameter represents a call-back function which is invoked in case of resetting all widgets.

    

 

$j(document).ready(function() {
  $j("#myCustomInteractiveFilterChoice").select2();
});  
  After loading the HTML page the registered JQuery method select2() is invoked to transform a simple HTML select to a complex value selector inclusive look-ahead behavior.
function rebuildCustomWatchListFilterUsers() {
  ...
}
  That function builds the list of selected users right below the selection box
    

 

function publishCustomWatchListFilter() {
  var objFilter = {
    id     : 'custom_watch_list_filter',
    table  : 'task',
    filter : arrItemSysId.length > 0 ? 
      'watch_listDYNAMIC' + arrItemSysId : 
      ''
  };

  ...
}
 

Based on the selected users, that function broadcasts a filter object (objFilter) to all dashboards widgets.

The most important part is the property "filter" where the encoded query portion is built which should be added to widgets which are following interactive filters and displaying records from task inherited tables.

Implemented is a query which combines all selected users with a OR condition and in case you want a AND behavior you have to modify the value at the "filter" property.

    

 

function removeCustomWatchListFilterItem(strSysId) {
  ...
}
function resetCustomWatchListFilter() {
  ...
}
function addCustomWatchListFilterItem(){
  ...
}
 

These functions take care of the management regarding the selected users.

      
<g:evaluate>
var arrUserList = [];
var grUser      = new GlideRecord('sys_user');

grUser.orderBy('name')
grUser.query();

while(grUser.next()) {
  arrUserList.push({
    name:  grUser.getValue('name'),
    sysId: grUser.getUniqueValue()
  });
}
</g:evaluate>
 

Preloading of all records of the sys_user table. The array arrUserList is later iterated to fill the entries for the user selection.

 

 

 

Resources

 

Comments
Sathwik1
Tera Guru

Thanks @Maik Skoddow for your efforts to post this one..

I am using approach 3.. and it was perfectly working..

but here a small doubt please help me on this..

In my dashboard there are two report one is on incident and other is on requested item and in the  both tabels there is a field called "User" which is a list collector..

so based on above code... I used for incident table and it is working.. and now I want to add for requested item as well.. please he me to modify the code..

 function publishCustomWatchListFilter()

{ var objFilter = { id : 'custom_watch_list_filter', table : 'task', filter : arrItemSysId.length > 0 ? 'watch_listDYNAMIC' + arrItemSysId : '' };

Greg Hoschar
Tera Contributor

@Maik Skoddow I've been attempting to use approach 3 in San Diego release. When I try to edit the "Watcher (via Custom Filter)" widget to check "Act as interactive filter", the option is not listed. I've also attempted this in a nearly clean San Diego PDI and the option still does not show up for a Dynamic Content Block.

GregHoschar_1-1674680078415.png

 

Does Approach 3 still work in San Diego release?

Is there some option or module I must activate to enable this feature?

Can I manually enable "Act as interactive Filter" by manipulating the Portal Preferences for this Widget?

 

GregHoschar_0-1674680031926.png

 

Thank you!

Sandi Wilburn
Tera Contributor

The custom interactive filter is brilliant and works perfectly for my use case. Thank you so much!

krysiajacobs
Giga Explorer

Maik, this is an incredibly insightful and useful article. A million thanks for posting it.

I'm using approach 2 for a complicated dashboard with charts on several different tables, with list field references to sys_user in custom fields. Works perfectly!

Uncle Rob
Kilo Patron

@Greg Hoschar @Maik Skoddow 
I'm having the exact same issue.  Add option 3 to the Dashboard as a Content Block and there's no "Act as Interactive Filter" on the settings:

RobertFedoruk_0-1694438885382.png

 

Maik Skoddow
Tera Patron

Hi @Uncle Rob 

Is the issue solved after our yesterday's discussion?

Uncle Rob
Kilo Patron

@Greg Hoschar @Maik Skoddow 
What I found after testing is that even though the Edit Widget pop up did not have the "Act as Interactive Filter" checkbox, it still very much acted as a filter.

IMPORTANT NOTE!  It only acted as a filter when the script in my Dynamic Content Block was correct.
It would only "do nothing" if there were errors in the script.  So if yours isn't working, check for errors!

 

Rayane Bendahma
Tera Contributor

Hi,

anyone has an idea on how to hide the list of record before performing any search ?

 

I mean, in the situation you have a lot of incident and you only want to see the ones assigned to a specific person, there is no need to load the list when you arrived on the page (better perf).

shembop
Tera Contributor

What I am noticing in my build (using option 2) is that the filter remains in place even when I close the dashboard. Is there a way to have the filter reset when you navigate away from the dashboard? The other thing I found is that the cascade filter name shows in the widget. Is there any way around that? I have "fixed it" by naming the filter as an instructional name. For instance, naming it "Type name to lookup user"

Overrid3
Tera Explorer

Hello, 

 

nice and clear explanation @Maik Skoddow , i was able to implement approach 3 but i have a question regarding one of the notes you left in the post.

why

  • It can be placed only once on a dashboard. ?

 

is there any way we can use approach 3 and make it work in multiple tabs in the dashboard?

 

Thanks in advance

Michael Gruber
Tera Contributor

@Maik Skoddow 

Could you please explain in more detail what you mean by "Select "task" as "Target table". This way the filter is more generic and can be used for all tables which extend the task table." ?

I want to create a cascading filter so that I can select a case number and use that to filter...

 

1 - All cases where the parent field = case number. (one list view - show related child records)

 

2 - All vendor incidents where the parent field of the parent case (parent.parent.number) = case number. (second list view - show all related vendor incidents attached to child cases of the parent incident)

 

regards

Michael Gruber

Steven Parker
Giga Sage

@Maik Skoddow 

 

Approach 2 is what I am working with, but how do we use a list collector variable in the Target Table?  I want to lookup the user in a list collector variable, but can't figure out how to do that.  I have tried the sc_item_option as the target table and "Value" as the field to use, but no luck there.  I am just trying to see how many tasks have a person's name in a list collector.

 

Struggling with this....

simonpullen
ServiceNow Employee

Hello All,

I have seen an issue with Approach 3, seems that after chrome 136 the filter fails to render, firefox and other browsers work fine, just chrome having the issue.

 

Hello All,

I have seen an issue with Approach 3, seems that after chrome 134 the filter fails to render, firefox and other browsers work fine, just chrome having the issue.

 

Was able to now find the issue it was a div class that was causing the issue, so removed the div class around this call and it fixed it.

Before:

<div class="form-horizontal container-fluid">
  <option value="all">ALL</option>
  <j:forEach var="jvar_type" items="${arrUserList}">
    <g:evaluate var="jvar_type" jelly="true">
      var strName  = jelly.jvar_type.name;
      var strSysId = jelly.jvar_type.sysId;
    </g:evaluate>
    <option value="${strSysId}" label="${strName}">${strName}</option>
  </j:forEach>
</div>

 After:


<option value="all">ALL</option>
<j:forEach var="jvar_type" items="${arrUserList}">
<g:evaluate var="jvar_type" jelly="true">
var strName = jelly.jvar_type.name;
var strSysId = jelly.jvar_type.sysId;
</g:evaluate>
<option value="${strSysId}" label="${strName}">${strName}</option>
</j:forEach>
IshikaJain
Tera Explorer

I used this approach and am getting this error - "Uncaught RangeError: Maximum call stack size exceeded" Any idea why would that be and how can it be resolved.

Michael Gruber
Tera Contributor

@Maik Skoddow 

I've got a cascading filter that allows me to select 1x case (to enable me to filter related records for a specific case), I like the idea of your jelly dynamic content, but is there a way to take the output of the filter to display the contents of 1x field in the dynamic content? i.e. short description field for example?

 

regards

Michael

Ravibodha8
Tera Explorer

I used this approach and am getting this error - "Uncaught RangeError: Maximum call stack size exceeded" Any idea why would that be and how can it be resolved.

vinnus
Tera Contributor

we have "VOC Survey Dashboard" as below

vinnus_0-1753446201594.png

 

all reports from "Metric Result" table.

requirement is :

'Data Version' field from Case table have 3 choice, cases got created through the record producers to the case table and Data version field choice will be updated in the case table from the record producers submission,

now we want create the 'Data Version' filter on the dash board we need to map some how with the case table 'Data Version' based the filter selection report will get fileted and show the data according to case table 'Data Version' choices. Thanks 

Version history
Last update:
‎09-11-2023 12:01 AM
Updated by:
Contributors