Hiding an Empty Shopping Cart

“I love it when a plan comes together.”
Col. John “Hannibal” Smith

On the stock header of a ServiceNow portal there is an image of a shopping cart. As you order things from the Service Catalog, the number of items in your cart is displayed in the lower right-hand corner of that image. If you didn’t come to the portal to order something, the empty cart just sits there at the top of the screen for no reason, so I thought it might be nice to just hide the cart until there was at least one item in it.

There are already a couple of other items in the header that only appear if there is something there, the list of Requests and the the list of Approvals. I figured that I could emulate the approach taken on those items and it would work the same way.

Most of the things that drive ServiceNow are built using ServiceNow, and you can find them in the system database and edit them to do what you want. Some things, however, are built into the system, and there is no way that you can modify them. There is also a third category, and that is those things that actually are housed in the system database, but are locked down as read-only or ACL-controlled such that you cannot modify them. The Header Menu widget that contains the shopping cart code happens to fall into that third category. There it was right in front of me, but it was not editable, even though I am a System Administrator and I was in the right Update Set.

Nothing motivates me more than being told that I am being prevented from doing something that I want to do, so I immediately went to work trying to figure out a way to make changes to the Header Menu widget. As it turns out, it wasn’t that hard to do.

The first thing to do was to use the hamburger menu to export the widget to XML:

Export to XML (This Record)

Once I had my own copy on my own machine, I could look at the code and figure out what need to be changed and then change it. Digging through the HTML, I found out that there was already an ng-show attribute on the shopping cart for another purpose, so I just needed to add my condition to the list, and that should do the trick. Here is the relevant section of code:

  <!-- Shopping cart stuff -->
 <li ng-if="::options.enable_cart && data.isLoggedIn" ng-show="::!accessibilityEnabled" class="dropdown hidden-xs header-menu-item" role="presentation">
  	<a href
       data-toggle="dropdown"
       id="cart-dropdown"
       uib-tooltip-template="'item-added-tooltip.html'"
       tooltip-placement="bottom"
       tooltip-trigger="'none'"
       tooltip-is-open="$parent.itemAddedTooltipOpen"
       title="${Your shopping cart currently has} {{cartItemCount}} ${items}"
       aria-label="${Shopping cart}"
       role="menuitem">
    	<i class="fa fa-shopping-cart" aria-hidden="true"></i>
      <span ng-bind-html="'${Cart}'" aria-hidden="true"></span>
      <span ng-if="cartItemCount > 0" aria-hidden="true" class="label label-as-badge label-primary sp-navbar-badge-count">{{cartItemCount}}</span>
		</a>
    <div class="dropdown-menu cart-dropdown">
      <sp-widget widget="data.cartWidget"></sp-widget>
    </div>
  </li>

A little further down in the code there was reference to a variable called cartItemCount. Using my puissant powers of deductive reasoning, I concluded, based on the name of the variable and its usage, that this variable contained the number of items in the cart. This is exactly what I needed for my condition, so I changed this line:

<li ng-if="::options.enable_cart && data.isLoggedIn" ng-show="::!accessibilityEnabled" class="dropdown hidden-xs header-menu-item" role="presentation">

… to this:

<li ng-if="::options.enable_cart && data.isLoggedIn" ng-show="cartItemCount > 0 && !accessibilityEnabled" class="dropdown hidden-xs header-menu-item" role="presentation">

Now that I have figured out what change to make and have implemented the change, all I need to do is get it back into the instance. Fortunately, ServiceNow provides a way to do that, and it has the added benefit of bypassing those pesky rules that were preventing me from editing the thing directly in the tool.

If you open the System Update Sets section and go all the way down to the bottom and select Update Sets to Commit, there will be a link down at the bottom of that page titled Import Update Set from XML. Click on that guy, and even though your exported and modified XML document is not technically an Update Set, you can pull it in via that process and it will update the widget with your changes.

Returning Exported XML Back to the Instance

Now when I am on a portal that uses the Header Menu widget, you only see the Shopping Cart when there is something in the cart. Whenever it is empty, it drops out of the view.

I like it!

Fun with the User Menu

“Any sufficiently advanced technology is indistinguishable from magic.”
Arthur C. Clarke

In the upper right-hand corner of the ServiceNow UI, you can click on your name and a little menu will drop down with a series of choices:

The ServiceNow User Menu

There is something similar on a ServiceNow Portal, with similar options. To easily move back and forth between the main Portal and the primary UI, I wanted to add an option to each environment’s User Menu to navigate to the other environment. On the Portal, this was easy enough to do by editing the HTML on the Stock Header. In the primary UI, however, this was not quite so easy.

The content of the drop-down User Menu is yet another one of those things that is not stored in the system database, but is rather an integral part of the product itself. As far as I can tell, there is no place to go where you can edit these options. But you know how that goes: if you can’t change what gets sent to the page, that doesn’t mean you can’t alter what is on the page once the page has been delivered. With a little View Source investigating, we can see that the menu is an HTML unordered list (<ul>) with a class of dropdown-menu. That’s enough to get a handle on things and build a quick little UI Script:

addLoadEvent(function() {
	try {
		var ssp = (window.top.$j("#self_service_link").length == 0);
		if (ssp) {
			var sspItem = "<li><a href='/sp' id='self_service_link'>" + getMessage("Self Service Portal") + "</a></li>";
			var menuList = window.top.$j("ul.dropdown-menu");
			if (menuList.length > 0) {
				menuList.prepend(sspItem);
			}
		}
	} catch(e) {
		//
	}
});

The script is pretty straightforward. First we check to make sure that we haven’t already stuffed the extra item out there (sometimes these things have a way of running more than one time), and if we haven’t, then we create the new menu item, locate the existing list, and prepend the new item to the existing list. After this has been activated, the menu now looks like this:

The modified User Menu

I was really quite proud of myself there for a while, and then I realized that on the Portal, the Profile option was still the first option and the ServiceNow option was the second option. By using the prepend function to insert the new menu item, I made it the first item on the menu instead of the second. That really disturbs my sense of The Way Things Ought To Be, so I needed to figure out how to make it the second option and not the first.

It’s true that I could have just reversed things on the Portal side of the house, which is much easier to manipulate, but that disturbs my sense of The Way Things Ought To Be as well. On a User Menu, the Profile link should always be the first and the Logout link should always be the last. Always. I can’t explain it, but that’s just The Way Things Ought To Be. Period.

I’m not going to reveal how many different things that I tried in an effort to insert the new item into the second slot, but let’s just say it’s more than I’d really care to discuss. In the end, I was able to cut and paste some code that I found out on the Interwebs and it did the trick, so that was good enough for me. Don’t ask me to explain how it works, because I haven’t a clue. Although I am pretty handy with Javascript, I don’t know much about AngularJS and I know even less about jQuery. This line of code is pure jQuery voodoo magic as far as I can tell, but it does satisfy rule #1: it works. Yes, that’s unquestionably Cargo Cult Coding, but I’ve never been too proud to leverage someone else’s code, even when I can’t possibly begin to fully understand it myself. You can see the updated script here:

addLoadEvent(function() {
	try {
		var ssp = (window.top.$j("#self_service_link").length == 0);
		if (ssp) {
			var sspItem = "<li><a href='/sp' id='self_service_link'>" + getMessage("Self Service Portal") + "</a></li>";
			var menuList = window.top.$j("ul.dropdown-menu");
			if (menuList.length > 0) {
				window.top.$j("ul.dropdown-menu li:eq(0)").after(sspItem);
			}
		}
	} catch(e) {
		//
	}
});

Now, I can actually look at the menu again without those uncomfortable Rain Man impulses to set things right:

The properly modified User Menu

Well, that’s it for today. If you want a copy to play around with, you can get one here.

OK, that works!

“A good plan, violently executed now, is better than a perfect plan next week.”
General George S. Patton

Well, it’s not the way that I like to do things, but it does work, so there is at least that. If I can ever stumble across a way to do it right, I will. For now, though, this will have to do. I never could find a modifiable source for either the pop-up e-mail form or the drop-down menu, so I eventually gave up on trying to send the right content for the page to the browser from the start. My fallback plan was to modify the page after it was rendered on the browser to achieve my desired results. That’s not really the way that I like to do things, but hey — It works!

I inspected the Email option of the drop-down menu and determined that it had an ID of email_client_open. Armed with that useful tidbit of information, I was able to create the following Client Script linked to the Incident table:

function onLoad() {
	var elem = gel('email_client_open');
	if (elem) {
		elem.onclick = function() {
			var dialog = new GlideDialogWindow('email_recipient_selector');
			dialog.setSize(400,400);
			dialog.setTitle('Select Recipient');
			dialog.render();
		};
	}
}

All in all, it took four independent components to pull this off: 1) the Client Script (above), 2) the UI Page that served as the content of the modal dialog, 3) a UI Script to run on the original Email Client pop-up to pull in the selections passed in the URL, and 4) a Script Include to go get the appropriate email addresses based on the user’s selections on the modal dialog. Now, when you click on the Email menu option, a new interim modal dialog pops up asking you to select the recipients of the email:

The UI Page is the most straightforward component of the lot. It’s just a standard page using standard approaches to collect information from the user and pass it along to the other elements in the chain. Here is the HTML in its entirety:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
  <div style="padding: 20px;">
    Please select the recipients to whom you would like to send e-mail from the list below:
    <input type="checkbox" style="margin: 0px 8px 5px 30px;" id="caller"/>Caller
    <input type="checkbox" style="margin: 0px 8px 5px 30px;" id="group"/>Assignment Group
    <input type="checkbox" style="margin: 0px 8px 5px 30px;" id="tech"/>Assigned Technician
    <button class="button" onclick="composeEmail();">Compose E-mail</button>
  </div>
</j:jelly>

And here is the associated script:

function composeEmail() {
    var caller = gel('caller').checked;
    var group = gel('group').checked;
    var tech = gel('tech').checked;
    if (caller || group || tech) {
        continueToOriginalEmailPopUp(caller, group, tech);
    } else {
        alert('You must select at least one recipient to send an e-mail');
    }
}
 
function continueToOriginalEmailPopUp(caller, group, tech) {
    var id = g_form.getUniqueValue();
    if (id) {
        var url = new GlideURL("email_client.do");
        url.addParam("sysparm_table", 'incident');
        url.addParam("sysparm_sys_id", id);
        url.addParam("sysparm_target", 'incident');
        if (caller) {
            url.addParam("sysparm_to_caller", "true");
        }
        if (group) {
            url.addParam("sysparm_to_group", "true");
        }
        if (tech) {
            url.addParam("sysparm_to_tech", "true");
        }
        popupOpenEmailClient(url.getURL() + g_form.serializeChangedAll());
        GlideDialogWindow.get().destroy();
    }
}

To read the new parameters added to the URL when the email client page comes up, I needed to attach a script to that page. Here again, the problem is that I have been unable to find any way to modify that page directly. I’m not too proud of the solution that I came up with to address this problem, but again, it does work. To make it happen, I created a global UI Script that will now be included on every single page on this instance, but will look at the current location to see if needs to do anything. If the current page is not the email client pop-up, then it does nothing, but it still has to ask, which is a virtually pointless exercise on any page other than the one that I want. Still, it’s a way to get something to run on that page, and that’s what I was after. Here is the code:

if (window.location.pathname == '/email_client.do') {
    setupRecipients();
}
 
function setupRecipients() {
    var qp = {};
    var qs = window.location.search.substring(1);
    var parts = qs.split('&');
    for (var i=0; i<parts.length; i++) {
        var temp = parts[i].split('=');
        qp[temp[0]] = decodeURIComponent(temp[1]);
    }
 
    if (qp['sysparm_table'] == 'incident') {
        var caller = qp['sysparm_to_caller'];
        var group = qp['sysparm_to_group'];
        var tech = qp['sysparm_to_tech'];
        var ga = new GlideAjax('EmailRecipientTool');
        ga.addParam('sysparm_name', 'getRecipients');
        ga.addParam('sysparm_sys_id', qp['sysparm_sys_id']);
        ga.addParam('sysparm_to_caller', caller);
        ga.addParam('sysparm_to_group', group);
        ga.addParam('sysparm_to_tech', tech);
        ga.getXML(function(response) {
            var string = response.responseXML.documentElement.getAttribute("answer");
            var recipient = JSON.parse(string);
            var spans = $(document.body).select(".address");
            for (var i=0; i<spans.length; i++) {
                spans[i].className = 'addressHighlight';
            }
            deleteHighlightedAddresses();
            for (var i=0; i<recipient.length; i++) {
                var input = {};
                input.value = recipient[i].email;
                addEmailAddressToList('MsgToUI', input);
            }
        });
    }
}

To fetch the requested recipient e-mail addresses from the system database, the above script makes an Ajax call to the last component in the solution, the Script Include developed for this purpose:

var EmailRecipientTool = Class.create();
EmailRecipientTool.prototype = Object.extendsObject(AbstractAjaxProcessor, {
 
    getRecipients: function() {
        var list = [];
        var gr = new GlideRecord('incident');
        gr.get(this.getParameter('sysparm_sys_id'));
        if (this.getParameter('sysparm_to_caller') == 'true') {
            this.addRecipient(list, {sys_id: gr.getDisplayValue('caller_id.sys_id'), name: gr.getDisplayValue('caller_id.name'), email: gr.getDisplayValue('caller_id.email')});
        }
        if (this.getParameter('sysparm_to_group') == 'true') {
            this.addGroup(list, gr.getValue('assignment_group'));
        }
        if (this.getParameter('sysparm_to_tech') == 'true') {
            this.addRecipient(list, {sys_id: gr.getDisplayValue('assigned_to.sys_id'), name: gr.getDisplayValue('assigned_to.name'), email: gr.getDisplayValue('assigned_to.email')});
        }
        return JSON.stringify(list);
    },
 
    addGroup: function(list, group) {
        if (group > '') {
            var gr = new GlideRecord('sys_user_grmember');
            gr.addQuery('group', group);
            gr.query();
            while (gr.next()) {
                this.addRecipient(list, {sys_id: gr.getDisplayValue('user.sys_id'), name: gr.getDisplayValue('user.name'), email: gr.getDisplayValue('user.email')});
            }
        }
    },
 
    addRecipient: function(list, recipient) {
        if (recipient.sys_id > '') {
            var duplicate = false;
            for (var i=0; i<list.length; i++) {
                if (recipient.sys_id == list[i].sys_id) {
                    duplicate = true;
                }
            }
            if (!duplicate) {
                list.push(recipient);
            }
        }
    },
    type: 'EmailRecipientTool'
});

Well, that’s it. If anyone is interested, I bundled all four parts into an Update Set, which you can grab here. I was able to do that, by the way, thanks to this tool, which I cannot recommend highly enough. I can’t even begin to count how many times I’ve used that since I first installed it. The folks that put that together and keep it maintained do very nice work indeed, and have both my admiration and my appreciation.

Well, that doesn’t work

“I have not failed. I have just found 10,000 ways that won’t work!”
Thomas Edison

It seemed like a fairly straightforward request. My typical response to these types of queries is, “Sure, I can do that,” without really looking into things to see what it might take or if it is even possible. But seriously, how hard could it be?

On the ServiceNow Incident form, there is an option on a drop-down menu labeled Email:

Clicking on this menu option brings up a form that allows you to compose and send an email to the person who reported the Incident. They liked this feature, but they wanted to expand on the concept. Would it be possible to put a series of check-boxes somewhere across the top to select one or more of Caller, Assignment Group, or Assignee?

Sure, no problem.

As soon as I got back to my computer, I started digging around to find the UI Page or Portal Page behind that form. Unfortunately, there is no UI Page or Portal Page behind that form. From what I can tell, unlike most forms and pages in this tool, this one is an integral part of the product and does not have its parts and pieces housed in the system database. That seemed unfortunate, but then I discovered Email Client Templates. There were some possibilities there, but still not what I was looking for. I ran down a few other promising-looking rabbit holes that all terminated in fairly substantial brick walls at the end, and then decided to step back and taker a broader view of things.

If I couldn’t put the check-boxes on the form itself, what if I created an intermediate form that I could control completely, and then passed the user’s selections to the email form via URL parameters? Basically, all I had to do was find out where the existing menu item led, replace that destination with my own intermediate form, and then have my form do whatever the original menu item did, but with the addition of the user’s choices. Let’s go find that menu definition and grab the current pointer and replace it with a new one … how hard could that be?

Well, as it turns out, that menu is not based on data stored in the system database as far as I can tell. I may have missed it somehow, but I dug around quite a bit and couldn’t find any place where that list of items could be modified in any way. It might be in there somewhere, but I certainly couldn’t find it.

So, maybe I spoke a little too soon when I said that I could do this. Still, there has got to be a way

It works!

“The main thing is to keep the main thing the main thing.”
— Stephen Covey, The 7 Habits of Highly Effective People

Since the moment that it first came out, I was a big fan of Steve McConnell‘s Code Complete. The idea that there was a right way and a wrong way to code things made perfect sense to my binary-centric way of looking at the world. Clean, precise, elegant, and efficient code is admirable goal, and it’s right up there near the top of my list of things for which I should always strive.

But it’s not at the very top.

Like Abraham Maslow‘s Hierarchy of Needs, some things are just more equal than others. My #1 requirement of my code is, was, and always will be passing this simple test: Does it work? Once we clear that threshold, we can make it pretty. We can make it clean. We can make it efficient. We can even make it elegant and stylish and amazingly beautiful. But only if it works. I’m not ashamed of my ugly code that does what it was intended to do, but I’m quite embarrassed of my luxuriously styled simple and elegant routines that fail that most important characteristic of them all. If it doesn’t work, none of the rest matters at all.

Coding to me has always been an iterative process. I’m drawn to the things that I’ve never done before, so I seem to always be staking out new territory and getting involved with things that I know little about. I try things. Most of these initial efforts tend to end in spectacular failure, but then I try something else. I’m constantly searching for that thing that works. Once I clear that bar, then I start asking if there are ways in which it could be done better. But if, and only if, I can first discover what works.

Hopefully, the things that I end up posting out here made it out here because they work. There may be a better way to have done it, or a more efficient approach to the problem, and I’m definitely always on the look-out for those, but it better work. If it doesn’t work, I would definitely like to know about it. I love to make things better, faster, cheaper, and oh so beautiful, so I am always open to hearing those ideas as well. But the number one test will always be that same old simple question: Does it work?