
- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
03-15-2021 03:05 PM - edited 11-28-2022 11:00 AM
Hello!
Introduction
Role required: content_admin
I want to share with you all a few dynamic content block templates I have put together for custom interactive filters.
Template Capabilities:
1. Multi-Table Compatible: Perhaps the biggest benefit is that these templates are capable of implementing their filters across multiple tables. OOTB choice filters require a widget per table, resulting in a less than ideal user experience. These templates, on the other hand, let you handle all your tables centrally from one widget.
2. Filter Choices Persist: Per Tab/Widget/User the selected filter saves to the canvas preferences and loads automatically each time the widget is loaded.
- Full Disclosure: I was never able to get this feature to function adequately due primarily to content blocks being in the same load order as reports - and semi-random, at that - which often allows reports to load before our custom filters. In such a scenario, the report reloads when the custom filter later renders and engages its default and I recall this reload only having roughly an 85% success at engaging said default, sometimes instead reloading with its full superset of data as if no filter was engaged. Sadly, I gave up on overcoming this, as I did not manage to find a way to dictate for our custom filters to always load before any report began to render.
3. Widget Destroy Cleanup: Persisted choices are cleaned up on both widget and tab destroy.
4. Manual Choice Capability: The Manual Choice template allows a user to build a manual choice for added flexibility and freedom to use custom logic.
5. Query Choice Builder: Via GlideAggregate the Choice Builder template can build a choice list rather unconventionally in a flexible and fast manner.
6. Fully Scoped Templates: These templates are scoped which means that more than one iteration of these templates can be placed on the same dashboard tab without the various iterations interfering with each other.
7. Consistent with OOTB Styling: These templates utilize the ootb jQuery select2 styling to help create a cohesive end-user experience when using a mixture of custom and ootb interactive filters.
Without further ado, here are the templates. These, with a little modification, should be able to fit a wide variety of use cases.
Choice Builder (Template 1)
Description: A template that builds a choice list for you from values found in your desired table's field. Any field.
Main Use: Since OOTB Interactive filters only work with reference fields, booleans, and fields with choices defined, this template opens up the use of the remainder of the field types plus the utility of building the choice list for you.
Advice: Use this for building a choice list from a field that has reasonable cardinality. Utilizing fields with high cardinality such as short description are not advised.
<?xml version="1.0" encoding="utf-8" ?> <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null"> <!-- User Defined Settings --> <g:evaluate> var widget_title = "Choice Builder v6.2 Template"; var choice_table = "sys_user"; var choice_name = "title"; var choice_label = "Title"; var showEmpty = false; var filterDebug = true; var tables_fields = JSON.stringify([ {table:"incident",field:"caller_id.title",}, {table:"sc_req_item",field:"request.requested_for.title",} ]); </g:evaluate> <!-- Stop Here If Previewing (Crash Prevention) --> <j:if test="${RP.isPreview()}"> <center>${gs.getMessage('No preview available')}</center> </j:if> <!-- Proceed If Not Previewing --> <j:if test="${!RP.isPreview()}"> <!-- Set Unique Identifier --> <g:evaluate> var uid = 'UID_' + Math.round(Math.random() * 1000000000000000) </g:evaluate> <!-- Build Choice List --> <g:evaluate var="jvar_cl" object="true" jelly="true"> var cl = new GlideChoiceList(); var ga = new GlideAggregate(choice_table); ga.addQuery(choice_name, '!=', ''); ga.groupBy(choice_name); ga.groupBy(choice_label); ga.orderBy(choice_label); ga.query(); while (ga.next()) { cl.add(ga.getValue(choice_name), ga[choice_name].getDisplayValue()); } if (showEmpty) { cl.add("NULL","(Empty)"); } cl; </g:evaluate> <script> <!-- Initialize Scoped Filter --> var container${uid} = document.getElementById('${uid}_display').closest('[data-uuid]'); var customFilter${uid} = { select: $j('#${uid}_select'), widgetId: container${uid}.getAttribute("data-original-widget-sysid"), canvasId: SNC.canvas.layoutJson.canvasSysId, eventsId: container${uid}.getAttribute("data-uuid"), setTitle: $j('#${uid}_display').closest('[data-uuid]').find('.grid-widget-header-title:first').html('<span>${widget_title}</span>'), removeFilter: function() { SNC.canvas.interactiveFilters.removeDefaultValue(customFilter${uid}.widgetId, true); dashboardMessageHandler${uid}.removeFilter(); }, customFilterUtil: { getDefaultValueByKey: function() {customFilter${uid}.customFilterUtil.Callback('getDefaultValueByKey')}, removeAllDefaultValues: function(event) {customFilter${uid}.customFilterUtil.Callback('removeAllDefaultValues',event)}, Callback: function(utility, event) { var ga = new GlideAjax('CustomFilterUtil'); ga.addParam('sysparm_name', utility); ga.addParam('sysparm_widget_id', customFilter${uid}.widgetId); ga.addParam('sysparm_canvas_id', customFilter${uid}.canvasId); ga.getXMLWait(); <!--not async to obtained default before load. Most noticeable side effect is slower widget refresh --> var response = ga.getAnswer() ? ga.getAnswer() : ''; if (response && response.length) { if (utility == "getDefaultValueByKey") { customFilter${uid}.select.val(JSON.parse(response)[0].filter.split("=")[1]) customFilter${uid}.select.change() if (${filterDebug}) { $j('#${uid}_debug').after('<span id="${uid}_persist" style="background-color: LightGreen;">Persisted filter found.</span>') } } else { customFilter${uid}.removeFilter() if (${filterDebug}) { alert('CUSTOM FILTER DESTROY\nWidget Title: ${widget_title}\nWidget ID: '+customFilter${uid}.widgetId+'\nCanvas ID: '+customFilter${uid}.canvasId+'\nEvent: '+event+'\nDefaults Deleted: '+parseInt(response)) } } } } }, wgtDelBtn: container${uid}.querySelector('button[data-original-title="Remove"]'), wgtRelBtn: container${uid}.querySelector('button[data-original-title="Refresh"]'), tabCfgBtn: document.getElementById("navbar").querySelector('button[data-original-title="Configuration"]'), tabDelBtn: document.getElementById("modalBtnPrimary"), destroyed: false, eventCallback: function(data) { <!-- EVENTS: Refresh, Remove, Configuration, DeleteDashboardTab, TabChange, resize, destroy --> <!-- Detect Event: EventHandlers return an element, eventbus.subscribe returns data.action --> var event = this instanceof Element ? (this.getAttribute("data-original-title") ? this.getAttribute("data-original-title") : 'DeleteDashboardTab') : (customFilter${uid}.destroyed ? data.action : (data.action == 'resize' ? 'resize' : 'TabChange')) <!-- Unbind Event Listeners --> if (event == 'Refresh' || event == 'Remove' || event == 'DeleteDashboardTab' || event == 'TabChange') { customFilter${uid}.removeFilter() customFilter${uid}.wgtDelBtn.removeEventListener("click",customFilter${uid}.eventCallback) customFilter${uid}.tabDelBtn.removeEventListener("click",customFilter${uid}.eventCallback) customFilter${uid}.tabCfgBtn.removeEventListener("click",customFilter${uid}.eventCallback) } <!-- Rebind Events After Configuration Resets Them --> if (event == 'Configuration') {setTimeout(bindEvents${uid}, 1000)} <!-- Handle Widget Destroy --> if (event == "Remove" || event == "DeleteDashboardTab") { customFilter${uid}.destroyed = true customFilter${uid}.customFilterUtil.removeAllDefaultValues(event) } }, }; <!-- Call Handler, Events Subscription, Default Filter, & jQuery's select2 --> dashboardMessageHandler${uid} = new DashboardMessageHandler(customFilter${uid}.widgetId) SNC.canvas.eventbus.subscribe(customFilter${uid}.eventsId,customFilter${uid}.eventCallback) customFilter${uid}.customFilterUtil.getDefaultValueByKey(); //load default value on widget load customFilter${uid}.select.select2(); //transforms select into combobox <!-- Bind Widget Reload/Delete & Tab Delete Listeners --> function bindEvents${uid}() { customFilter${uid}.wgtRelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true}) customFilter${uid}.wgtDelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true}) customFilter${uid}.tabCfgBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true}) customFilter${uid}.tabDelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true}) } bindEvents${uid}() <!-- Function for Handling Changes --> function selectChange${uid}(selection) { value${uid} = selection.value; tablesFields${uid} = JSON.parse("${tables_fields}"); if (value${uid} == "All") { customFilter${uid}.removeFilter(); } else { var finalFilter${uid} = []; for (var i = 0; i != tablesFields${uid}.length; i++) { finalFilter${uid}.push({table: tablesFields${uid}[i].table, filter: tablesFields${uid}[i].field + '=' + value${uid}, }); } SNC.canvas.interactiveFilters.setDefaultValue({id: customFilter${uid}.widgetId, filters: finalFilter${uid}}, true); dashboardMessageHandler${uid}.publishMessage(finalFilter${uid}); } } <!-- Add Debug Messages If Debug On --> if (${filterDebug}) {$j('#${uid}_debug').append('<p style="line-height: 8px; padding-top: 8px;"><b>UID</b>: ${uid}<br/><b>Widget ID</b>: '+customFilter${uid}.widgetId+'<br/><b>Canvas ID</b>: '+customFilter${uid}.canvasId+'<br/><b>Events ID</b>: '+customFilter${uid}.eventsId+'</p>');} </script> <!-- Place Filter On Screen --> <div id="${uid}_display" class="select2-container form-control"> <select id="${uid}_select" class="form-control" value="All" onchange="selectChange${uid}(this)"> <option value="All">All</option> <g:options choiceList="${jvar_cl}" /> </select> </div> <div id="${uid}_debug"></div> <!-- End of Preview Prevention --> </j:if> </j:jelly>
Manual Choice (Template 2)
Description: A template that lets you build a custom choice list.
Use Case: For building a choice list utilizing either a) field values (partial string match, custom date logic) or b) operators (contains, startswith, etc) not available to ootb interactive filters.
<?xml version="1.0" encoding="utf-8" ?> <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null"> <!-- User Defined Settings --> <g:evaluate> var widget_title = "Manual Choice v6.2 Template"; var choice_table = "sys_user"; var choice_name = "city"; var choice_label = "City"; var showEmpty = false; var filterDebug = true; var tables_fields = JSON.stringify([ {table:"incident",field:"caller_id.city",}, {table:"sc_req_item",field:"request.requested_for.city",} ]); var manual_choices = [ {label:"Nashville",value:"nashville"}, {label:"Detroit",value:"detroit"} ]; </g:evaluate> <!-- Stop Here If Previewing (Crash Prevention) --> <j:if test="${RP.isPreview()}"> <center>${gs.getMessage('No preview available')}</center> </j:if> <!-- Proceed If Not Previewing --> <j:if test="${!RP.isPreview()}"> <!-- Set Unique Identifier --> <g:evaluate> var uid = 'UID_' + Math.round(Math.random() * 1000000000000000) </g:evaluate> <!-- Build Manual List --> <g:evaluate var="jvar_cl" object="true" jelly="true"> var cl = new GlideChoiceList(); for (var i = 0; i != manual_choices.length; i++) { cl.add(manual_choices[i].value, manual_choices[i].label); } if (showEmpty) { cl.add("NULL","(Empty)"); } cl; </g:evaluate> <script> <!-- Initialize Scoped Filter --> var container${uid} = document.getElementById('${uid}_display').closest('[data-uuid]'); var customFilter${uid} = { select: $j('#${uid}_select'), widgetId: container${uid}.getAttribute("data-original-widget-sysid"), canvasId: SNC.canvas.layoutJson.canvasSysId, eventsId: container${uid}.getAttribute("data-uuid"), setTitle: $j('#${uid}_display').closest('[data-uuid]').find('.grid-widget-header-title:first').html('<span>${widget_title}</span>'), removeFilter: function() { SNC.canvas.interactiveFilters.removeDefaultValue(customFilter${uid}.widgetId, true); dashboardMessageHandler${uid}.removeFilter(); }, customFilterUtil: { getDefaultValueByKey: function() {customFilter${uid}.customFilterUtil.Callback('getDefaultValueByKey')}, removeAllDefaultValues: function(event) {customFilter${uid}.customFilterUtil.Callback('removeAllDefaultValues',event)}, Callback: function(utility, event) { var ga = new GlideAjax('CustomFilterUtil'); ga.addParam('sysparm_name', utility); ga.addParam('sysparm_widget_id', customFilter${uid}.widgetId); ga.addParam('sysparm_canvas_id', customFilter${uid}.canvasId); ga.getXMLWait(); <!--not async to obtained default before load. Most noticeable side effect is slower widget refresh --> var response = ga.getAnswer() ? ga.getAnswer() : ''; if (response && response.length) { if (utility == "getDefaultValueByKey") { customFilter${uid}.select.val(JSON.parse(response)[0].filter.split("=")[1]) customFilter${uid}.select.change() if (${filterDebug}) { $j('#${uid}_debug').after('<span id="${uid}_persist" style="background-color: LightGreen;">Persisted filter found.</span>') } } else { customFilter${uid}.removeFilter() if (${filterDebug}) { alert('CUSTOM FILTER DESTROY\nWidget Title: ${widget_title}\nWidget ID: '+customFilter${uid}.widgetId+'\nCanvas ID: '+customFilter${uid}.canvasId+'\nEvent: '+event+'\nDefaults Deleted: '+parseInt(response)) } } } } }, wgtDelBtn: container${uid}.querySelector('button[data-original-title="Remove"]'), wgtRelBtn: container${uid}.querySelector('button[data-original-title="Refresh"]'), tabCfgBtn: document.getElementById("navbar").querySelector('button[data-original-title="Configuration"]'), tabDelBtn: document.getElementById("modalBtnPrimary"), destroyed: false, eventCallback: function(data) { <!-- EVENTS: Refresh, Remove, Configuration, DeleteDashboardTab, TabChange, resize, destroy --> <!-- Detect Event: EventHandlers return an element, eventbus.subscribe returns data.action --> var event = this instanceof Element ? (this.getAttribute("data-original-title") ? this.getAttribute("data-original-title") : 'DeleteDashboardTab') : (customFilter${uid}.destroyed ? data.action : (data.action == 'resize' ? 'resize' : 'TabChange')) <!-- Unbind Event Listeners --> if (event == 'Refresh' || event == 'Remove' || event == 'DeleteDashboardTab' || event == 'TabChange') { customFilter${uid}.removeFilter() customFilter${uid}.wgtDelBtn.removeEventListener("click",customFilter${uid}.eventCallback) customFilter${uid}.tabDelBtn.removeEventListener("click",customFilter${uid}.eventCallback) customFilter${uid}.tabCfgBtn.removeEventListener("click",customFilter${uid}.eventCallback) } <!-- Rebind Events After Configuration Resets Them --> if (event == 'Configuration') {setTimeout(bindEvents${uid}, 1000)} <!-- Handle Widget Destroy --> if (event == "Remove" || event == "DeleteDashboardTab") { customFilter${uid}.destroyed = true customFilter${uid}.customFilterUtil.removeAllDefaultValues(event) } }, }; <!-- Call Handler, Events Subscription, Default Filter, & jQuery's select2 --> dashboardMessageHandler${uid} = new DashboardMessageHandler(customFilter${uid}.widgetId) SNC.canvas.eventbus.subscribe(customFilter${uid}.eventsId,customFilter${uid}.eventCallback) customFilter${uid}.customFilterUtil.getDefaultValueByKey(); //load default value on widget load customFilter${uid}.select.select2(); //transforms select into combobox <!-- Bind Widget Reload/Delete & Tab Delete Listeners --> function bindEvents${uid}() { customFilter${uid}.wgtRelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true}) customFilter${uid}.wgtDelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true}) customFilter${uid}.tabCfgBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true}) customFilter${uid}.tabDelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true}) } bindEvents${uid}() <!-- Function for Handling Changes --> function selectChange${uid}(selection) { value${uid} = selection.value; tablesFields${uid} = JSON.parse("${tables_fields}"); if (value${uid} == "All") { customFilter${uid}.removeFilter(); } else { var finalFilter${uid} = []; for (var i = 0; i != tablesFields${uid}.length; i++) { finalFilter${uid}.push({table: tablesFields${uid}[i].table, filter: tablesFields${uid}[i].field + '=' + value${uid}, }); } SNC.canvas.interactiveFilters.setDefaultValue({id: customFilter${uid}.widgetId, filters: finalFilter${uid}}, true); dashboardMessageHandler${uid}.publishMessage(finalFilter${uid}); } } <!-- Add Debug Messages If Debug On --> if (${filterDebug}) {$j('#${uid}_debug').append('<p style="line-height: 8px; padding-top: 8px;"><b>UID</b>: ${uid}<br/><b>Widget ID</b>: '+customFilter${uid}.widgetId+'<br/><b>Canvas ID</b>: '+customFilter${uid}.canvasId+'<br/><b>Events ID</b>: '+customFilter${uid}.eventsId+'</p>');} </script> <!-- Place Filter On Screen --> <div id="${uid}_display" class="select2-container form-control"> <select id="${uid}_select" class="form-control" value="All" onchange="selectChange${uid}(this)"> <option value="All">All</option> <g:options choiceList="${jvar_cl}" /> </select> </div> <div id="${uid}_debug"></div> <!-- End of Preview Prevention --> </j:if> </j:jelly>
Script Include (CustomFilterUtil)
Description: A script include that enables a dashboard's viewer to access sys_canvas_preferences with the dashboard's custom filters.
Use Case: For loading persisted filters on page load and for deleting persisted filters of a destroyed widget on widget destroy.
Client callable: true
var CustomFilterUtil = Class.create(); CustomFilterUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, { // Dynamic Content Block GlideRecord access constraints to sys_canvas_preferences and the incompatibility of // SNC.canvas.interactiveFilters.getDefaultValueByKey with custom filters necessitate the use of this utility. getDefaultValueByKey: function(){ var grCanvasPreferences = new GlideRecord('sys_canvas_preferences'); grCanvasPreferences.addQuery('user', gs.getUserID()); grCanvasPreferences.addQuery('widget_id', this.getParameter("sysparm_widget_id")); grCanvasPreferences.addQuery('canvas_page', this.getParameter("sysparm_canvas_id")); grCanvasPreferences.query(); grCanvasPreferences.next(); return grCanvasPreferences.getValue('value'); }, removeAllDefaultValues: function(){ var delCount = 0; var grCanvasPreferences = new GlideRecord('sys_canvas_preferences'); grCanvasPreferences.addQuery('widget_id', this.getParameter("sysparm_widget_id")); grCanvasPreferences.addQuery('canvas_page', this.getParameter("sysparm_canvas_id")); grCanvasPreferences.query(); while (grCanvasPreferences.next()) { grCanvasPreferences.deleteRecord(); delCount++; } return delCount; }, type: 'CustomFilterUtil' });
Miscellaneous Details
Widget Titles: I've added a function that will set the widget's header. Just set the 'widget_title' variable in the User Defined Settings portion at the top of each filter and you'll be good to go.
Script Include: I threw in a script include! Mainly because I couldn't get the OOTB getDefaultValuesByKey() to work on page load - only on widget refresh. This has the added benefit of letting one know how many preferences were deleted, too.
Event Handling: I added a handful of event listeners to ensure that the relevant actions are handled, to promote a culture of cleanup regarding the saved preferences. The eventbus's data.actions weren't granular enough to provide this functionality alone - especially since deleting a widget, deleting a tab, and changing tabs are all considered a "destroy" event - but we don't want to delete defaults, for example, on tab changes, haha! No problem, we've got it covered in this template.
Lazy Loading: Dashboards have a feature called "Lazy loading" that causes only visible widgets to load. However, the order that said widgets are loaded in is using an algorithm I have not been able to identify. It is not top-to-bottom, left-to-right. This seemingly random load order means that these custom filters cannot be guaranteed to engage prior to the first report that renders. If a report renders first then it will reload when our filter is rendered and engages its default, though sometimes the report reloads without the default properly engaged. Please let me know if you know how I can make these dynamic content blocks pre-empt lazy loading.
Debug: I've added a debug mode which displays the key IDs of each widget onscreen from the dashboard and alerts regarding the canvas preferences deletion context for convenience. To turn it on, just set the filterDebug variable of said dynamic content block to true. You can add the debug homepage filters widget to your tab as well for easy debugging.
End
Please, if anyone has any improvements, feel free to share.
Fun Additional Reading:
1.Building custom visualizations and interactive filters (CreatorCon 2019) (INCREDIBLY USEFUL!)
*This lab has a ton of great reusable concepts in its code, which I used heavily here in a dumbed down and altered fashion, especially in scoping the filter in order to make it cooperate with other iterations of itself. This lab's filters are far superior to my templates and I would have absolutely just used this labs content - and never made these templates - if it wasn't for a glitch in the lab's filters that was killing their implementation - which @Adam Stout just recently mentioned how to fix. The fix's details can be found in this community question.
2. Interactive Filter - Display Field (Community Question)
* @Ararana Thank you! I built upon your excellent work here! 🙂
3. Date Range Filter of Dashboard
* @Christy Anusha Thank you! I used your method to get SNC.canvas.eventbus.subscribe() to work on widget destroy events!
4. Custom interactive filter example - Task filter (Product Doc)
5. Custom interactive filter example - Multiple reports (Product Doc)
6. jQuery's Select2 Official Documentation
7. Interlocked Category/Subcategory Custom Interactive Filters
I hope someone finds this useful! I hope it empowers you to provide better service, just like this community has empowered me. Please mark this post as helpful or bookmark it if you find it helpful. Thanks!
Kind Regards,
Joseph
- 7,173 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Great to see. Be sure to write some ATF tests to make sure these keep working. See more info on this here: https://community.servicenow.com/community?id=community_blog&sys_id=9130c1bcdb7518d0414eeeb5ca961955
As for the 2019 lab, they were writing pre-Orlando and broke due to a fix in Orlando which enforced something that I had wrong in the lab where I didn't call setDefaultValue before publishing the message. It is an easy fix if you have the code but I can't easily change what was published in the past.

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks
Especially for the comment on the lab, I wouldn't have found that without your help. I used your advice to find the fix and documented it in my answer to this community question. There's still a bit of an issue with the multi-list of the multi-choice interactive filter. It's got dementia; it forgets its selection on page reloads.

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I've updated the templates to persist their filters. Now if I could figure out how to override lazy loading and load the content block (and its persisted filter) before any of the dashboard's reports begin to load regardless of where it placed on the dashboard...

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I've updated the templates again with a new CustomFilterUtil script include and event listeners for handling loading persisting filters on page load and for deleting all preferences of a destroyed widget on widget/tab destroy.
I hope this helps!
Kind Regards,
Joseph

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I've updated the templates again with jQuery's select2() function. This transforms the legacy-looking <select> boxes and their dropdowns into the usual combobox style with a text-lookup that we are used to seeing.
Here's a preview:
Enjoy!
Kind Regards,
Joseph
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@JosephW1 : Thanks for sharing this article and its very helpful. I am curious to know if this choice list can be used with multiple choice rather than single selection. If yes can you provide some insights on that or any script example where we need to make the changes. It always nice to select multiple option rather than single choice now a days.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thank you, I used your logic on the choice list to flish out Business Unit on the Bussiness Service Table
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@JosephW1 @Adam Stout : Can it be possible to add a conditional statement for a custom interactive filter on the dashboard ? I want such a functionality using which the dynamic content filter dashboard should be available according to the logged-in user's role. I tried but it's not working. its not printing code also. Looking for support.

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@Amit Naik1 I apologize, but please reiterate this main portion of your request, as it does not make sense to me and so I'm unable to figure out what you're asking:
"I want such a functionality using which the dynamic content filter dashboard should be available according to the logged-in user's role." - Amit Naik1
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@JosephW1 : "I want such a functionality using which the dynamic content filter dashboard should be available according to the logged-in user's role."
Reiterate - I want to show the custom interactive filter on dashboard to specific users which contains certain roles and other people who doesnt contains specific roles should not see that custom interactive filter in dashboard. I see there is condition field available in dynamic content can we use that for this type of requirement and how ? because its not working for me.

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@Amit Naik1 Thanks, I understand now. You have a cool idea, there! Definitely not an OOTB feature, so it will take some creativity. That's the fun part, though, right?
What you're trying is definitely doable, it just requires the following:
- Understanding that what you want to be done - hiding the entire widget or blanking it out - cannot be done via the dynamic content block's condition but rather must be done via custom code that reads from the client and is able to act on the page.
- If there's a way to do this from the condition fields, please let me know! I think the condition code runs server-side, so I'd guess it would require the gs.hasRole method if it worked for this at all, yet I tried both answer = gs.hasRole('admin'); and gs.hasRole('admin'); and neither changed any of the widget's contents for non-admin users for me in my testing.
- Understanding how to obtain and compare against the user's roles (they are client-side and therefore you need to read them from a script tag)
- Understanding how to grab the current widget
- Understanding how to empty a widget and/or to use the GridStackAPI to remove a given widget from the screen (depends on your desired end result)
Most of this knowledge is not utilizing documented APIs, so undertaking this sort of task requires dedication and a lot of self-research to self-learn these APIs, so be ready to saddle up and dig in. If you enjoy this sort of stuff, though, it is a fun ride. 😁
Here is a sample widget that hides itself based on the user's role and written in a way that should be easily re-usable. This should give you a jump-start on understanding the items above.
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<!-- Stop Here If Previewing (Crash Prevention) -->
<j:if test="${RP.isPreview()}">
<center>${gs.getMessage('No preview available')}</center>
<j:break/>
</j:if>
<!-- Set Unique Identifier (Use this so that multiple instances of this block, on the same dashboard tab, don't interere with each other. -->
<g:evaluate>
var uid = 'UID_' + Math.round(Math.random() * 1000000000000000)
</g:evaluate>
<script>
// Determine if user has target role
var userRoles = NOW.user.roles.split(',');
var targetRoles = ['admin', 'dashboard_admin','pa_admin','report_admin','content_admin'];
var hasTargetRole = userRoles.filter(function(role) {
return targetRoles.indexOf(role) > -1;
}).length > 0;
// Hide widget if user lacks target role
if (!hasTargetRole) {
var $widget = $j('#${uid}_display').closest('[data-uuid]');
var method = {fullRemove: true, whiteOut: false};
if (method.whiteOut) { //this would just white-out the widget's header and contents
$widget.empty();
}
if (method.fullRemove) { //this temporarily removes the widget, which could cause the gridStack to shift and unalign visually
var gridStackAPI = $j('.grid-stack').data('gridstack');
gridStackAPI.removeWidget($widget);
}
}
</script>
<!-- Place Display -->
<div id="${uid}_display"/>
</j:jelly>

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@Amit Naik1 Oh, regarding your earlier post about multi-selection, it also can be done; just gotta know how! That's the nice thing about being able to write the code for dynamic content blocks, as it makes the sky the limit for a lot of scenarios that aren't baked into other OOTB element frameworks such as the OOTB interactive filters framework. (Of course, the OOTB interactive filters framework supports multi-choice filters it's just that for fields unsupported by that framework, that require custom filters, we need to know how to build these multi-choice filters from scratch via our custom code.)
Here is a filter I've made in the past that has multi-selection. Please be warned that I rarely use this method - this is the only such filter I've made and I don't ever reuse it - and so its framework hasn't been very thoroughly refined or field tested. My customers don't have many use cases for this, I guess, plus I don't like how wide multi-choice filters seem to need to be and how much screen real-estate that takes up.
<?xml version="1.0" encoding="utf-8" ?> <!-- AUTHOR: Joseph Waters 07/30/2021 -->
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<!-- User Defined Settings -->
<g:evaluate>
var widget_title = "Company";
var choice_table = "sys_user";
var choice_name = "u_company";
var choice_label = "Company";
var showEmpty = false;
var filterDebug = false;
var tables_fields = JSON.stringify([
{table:'cmdb_sam_sw_install',field:'installed_on.assigned_to.u_company'},
{table:'cmdb_ci_computer',field:'assigned_to.u_company'},
{table:'incident',field:'configuration_item.assigned_to.u_company'},
{table:'cmdb_health_result',field:'ci.assigned_to.u_company'}
]);
</g:evaluate>
<!-- Stop Here If Previewing (Crash Prevention) -->
<j:if test="${RP.isPreview()}">
<center>${gs.getMessage('No preview available')}</center>
</j:if>
<!-- Proceed If Not Previewing -->
<j:if test="${!RP.isPreview()}">
<!-- Set Unique Identifier -->
<g:evaluate>
var uid = 'UID_' + Math.round(Math.random() * 1000000000000000)
</g:evaluate>
<!-- Build Choice List -->
<g:evaluate var="jvar_cl" object="true" jelly="true">
var cl = new GlideChoiceList();
var ga = new GlideAggregate(choice_table);
ga.addQuery(choice_name, '!=', '');
ga.addQuery('active', true);
ga.groupBy(choice_name);
ga.groupBy(choice_label);
ga.orderBy(choice_label);
ga.query();
while (ga.next()) {
cl.add(ga.getValue(choice_name), ga[choice_name].getDisplayValue());
}
if (showEmpty) {
cl.add("NULL","(Empty)");
}
cl;
</g:evaluate>
<script>
<!-- Initialize Scoped Filter -->
var container${uid} = document.getElementById('${uid}_display').closest('[data-uuid]');
var customFilter${uid} = {
select: $j('#${uid}_select'),
selectMultiple: $j('#${uid}_select_multiple'),
selectedValue: function(){return $j('#${uid}_select').val()},
selectedLabel: function(){return $j('#${uid}_display').find('span.select2-chosen').html()}, <!-- more reliable than option:selected -->
widgetId: container${uid}.getAttribute("data-original-widget-sysid"),
canvasId: SNC.canvas.layoutJson.canvasSysId,
eventsId: container${uid}.getAttribute("data-uuid"),
setTitle: $j('#${uid}_display').closest('[data-uuid]').find('.grid-widget-header-title:first').html('<span>${widget_title}</span>'),
removeFilter: function() {
<!-- SNC.canvas.interactiveFilters.removeDefaultValue(customFilter${uid}.widgetId, true); -->
SNC.canvas.interactiveFilters.removeDefaultValue(customFilter${uid}.widgetId, false);
dashboardMessageHandler${uid}.removeFilter();
},
resetFilter: function() {
customFilter${uid}.select.val("All");
customFilter${uid}.select.change();
},
customFilterUtil: {
getDefaultValueByKey: function() {customFilter${uid}.customFilterUtil.Callback('getDefaultValueByKey')},
removeAllDefaultValues: function(event) {customFilter${uid}.customFilterUtil.Callback('removeAllDefaultValues',event)},
Callback: function(utility, event) {
var ga = new GlideAjax('CustomFilterUtil');
ga.addParam('sysparm_name', utility);
ga.addParam('sysparm_widget_id', customFilter${uid}.widgetId);
ga.addParam('sysparm_canvas_id', customFilter${uid}.canvasId);
ga.getXMLWait(); <!--not async to obtained default before load. Most noticeable side effect is slower widget refresh -->
var response = ga.getAnswer() ? ga.getAnswer() : '';
if (response && response.length) {
if (utility == "getDefaultValueByKey") {
customFilter${uid}.select.val(JSON.parse(response)[0].filter.split("=")[1])
customFilter${uid}.select.change()
if (${filterDebug}) {
$j('#${uid}_debug').after('<span id="${uid}_persist" style="background-color: LightGreen;">Persisted filter found.</span>')
}
} else {
customFilter${uid}.removeFilter()
if (${filterDebug}) {
alert('CUSTOM FILTER DESTROY\nWidget Title: ${widget_title}\nWidget ID: '+customFilter${uid}.widgetId+'\nCanvas ID: '+customFilter${uid}.canvasId+'\nEvent: '+event+'\nDefaults Deleted: '+parseInt(response))
}
}
}
}
},
wgtDelBtn: container${uid}.querySelector('button[data-original-title="Remove"]'),
wgtRelBtn: container${uid}.querySelector('button[data-original-title="Refresh"]'),
tabDelBtn: document.getElementById("modalBtnPrimary"),
tabRefBtn: document.querySelector('[ng-click="refreshAllPanes()"]'),
tabResBtn: document.querySelector('[ng-click="resetAllFilters()"]'),
destroyed: false,
eventCallback: function(data) { <!-- EVENTS: Refresh, Remove, Configuration, DeleteDashboardTab, TabChange, resize, destroy -->
<!-- Detect Event: EventHandlers return an element, eventbus.subscribe returns data.action -->
var event = this instanceof Element ? (this.getAttribute("data-original-title") ? this.getAttribute("data-original-title") : (this.innerText == 'Delete' ? 'DeleteDashboardTab' : 'Tab ' + this.innerText)) : (customFilter${uid}.destroyed ? data.action : (data.action == 'resize' ? 'resize' : 'TabChange'))
<!-- Abort if Configuration -->
if (event == 'Configuration') {return;};
<!-- Unbind Event Listeners -->
if (event == 'Refresh' || event == 'Remove' || event == 'Tab Refresh' || event == 'DeleteDashboardTab' || event == 'TabChange') {
customFilter${uid}.wgtRelBtn.removeEventListener("click",customFilter${uid}.eventCallback);
customFilter${uid}.wgtDelBtn.removeEventListener("click",customFilter${uid}.eventCallback);
customFilter${uid}.tabDelBtn.removeEventListener("click",customFilter${uid}.eventCallback);
customFilter${uid}.tabRefBtn.removeEventListener("click",customFilter${uid}.eventCallback);
customFilter${uid}.tabResBtn.removeEventListener("click",customFilter${uid}.eventCallback);
}
<!-- Reset Filters -->
if (event == 'Refresh' || event == 'Remove' || event == 'Tab Reset Filters') {
if (customFilter${uid}.selectedValue() != "All" || event == 'Tab Reset Filters') {
customFilter${uid}.resetFilter();
}
}
<!-- Delay Tab Refresh -->
if (event == 'Tab Refresh') { <!-- THIS DOESN'T WORK. -->
<!-- customFilter${uid}.resetFilter();
setTimeout(customFilter${uid}.resetFilter,Math.floor(Math.random() * 300) + 1);
setTimeout(customFilter${uid}.resetFilter,Math.floor(Math.random() * 300) + 1); -->
}
<!-- Handle Widget Destroy -->
if (event == "Remove" || event == "DeleteDashboardTab") {
customFilter${uid}.destroyed = true;
<!-- customFilter${uid}.customFilterUtil.removeAllDefaultValues(event); -->
}
},
};
<!-- Call Handler, Events Subscription, Default Filter, & jQuery's select2 -->
dashboardMessageHandler${uid} = new DashboardMessageHandler(customFilter${uid}.widgetId)
SNC.canvas.eventbus.subscribe(customFilter${uid}.eventsId,customFilter${uid}.eventCallback)
<!-- customFilter${uid}.customFilterUtil.getDefaultValueByKey(); //load default value on widget load -->
customFilter${uid}.select.select2(); //transforms select into combobox
customFilter${uid}.selectMultiple.select2();
<!-- Bind Widget Reload/Delete & Tab Delete Listeners -->
function bindEvents${uid}() {
customFilter${uid}.wgtRelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true})
customFilter${uid}.wgtDelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true})
customFilter${uid}.tabDelBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true})
customFilter${uid}.tabRefBtn.addEventListener("click",customFilter${uid}.eventCallback,{once:true});
customFilter${uid}.tabResBtn.addEventListener("click",customFilter${uid}.eventCallback);
}
bindEvents${uid}()
<!-- Functions for Handling Changes -->
function selectChange${uid}(select) { <!-- This select only pushes values to the selectMultiple -->
if (select.value == 'All') {
if (customFilter${uid}.selectMultiple.select2('val') == '') {return;}
customFilter${uid}.selectMultiple.select2('val','');
} else {
customFilter${uid}.selectMultiple.select2('val', customFilter${uid}.selectMultiple.select2('val').concat(select.value));
}
customFilter${uid}.selectMultiple.change();
}
function selectMultipleChange${uid}(selectMultiple) { <!-- This select is where the filter is handled. -->
var values${uid} = customFilter${uid}.selectMultiple.select2('val');
tablesFields${uid} = JSON.parse("${tables_fields}");
if (values${uid} == "") {
customFilter${uid}.removeFilter();
} else {
var finalFilter${uid} = [];
for (var i = 0; i != tablesFields${uid}.length; i++) {
finalFilter${uid}.push({table: tablesFields${uid}[i].table, filter: tablesFields${uid}[i].field + 'IN' + values${uid},
});
}
<!-- SNC.canvas.interactiveFilters.setDefaultValue({id: customFilter${uid}.widgetId, filters: finalFilter${uid}}, true); -->
SNC.canvas.interactiveFilters.setDefaultValue({id: customFilter${uid}.widgetId, filters: finalFilter${uid}}, false);
dashboardMessageHandler${uid}.publishMessage(finalFilter${uid});
}
}
<!-- Add Debug Messages If Debug On -->
if (${filterDebug}) {$j('#${uid}_debug').append('<p style="line-height: 8px; padding-top: 8px;"><b>UID</b>: ${uid}<br/><b>Widget ID</b>: '+customFilter${uid}.widgetId+'<br/><b>Canvas ID</b>: '+customFilter${uid}.canvasId+'<br/><b>Events ID</b>: '+customFilter${uid}.eventsId+'</p>');}
</script>
<!-- Place Filter On Screen -->
<div id="${uid}_display" class="select2-container form-control">
<select id="${uid}_select" class="form-control" value="All" onchange="selectChange${uid}(this)">
<option value="All">All</option>
<g:options choiceList="${jvar_cl}" />
</select>
<select id='${uid}_select_multiple' class="form-control interactive-filter__widget-content" multiple='multiple' onchange="selectMultipleChange${uid}(this)">
<g:options choiceList="${jvar_cl}" />
</select>
</div>
<div id="${uid}_debug"></div>
<!-- End of Preview Prevention -->
</j:if>
</j:jelly>
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@JosephW1 thanks for this great feature and the code.
Could you explain the mechanism behind the persisting filter? How is the selected option stored when the page is reloaded? This feature does not work in my case for the code you shared.
Update: I discovered I needed to create the Script Include in the System UI->Script Includes section of ServiceNow. Things are working now.
Another question @JosephW1 : did you ever discover how to make the dynamic content blocks pre-empt lazy loading?