Reference Type System Properties, Part III

“Everything should be made as simple as possible, but not simpler.”
Albert Einstein

Last time, we got far enough along in the System Property value page modifications to demonstrate that we could replace the rendered input element with something else of our own design. Not having a design of our own for an adequate replacement, we implemented the popular creative avoidance strategy by working on all of the other parts and pieces first until we finally ran out of other parts and pieces. Now it is time to come up with a plan and finish this little project up.

I have to admit that I’m not all that proud of what I eventually came up with, but it does satisfy Rule #1, so at this point, I’ll take it and call it good. I tried a number of other things first, but none of those really got me what I wanted, so here we are. The basic plan is pretty simple, and consists of two parts: 1) a hidden input element to replace the text input element so that it can be submitted with the form, and 2) an iframe into which we will put our new input element via a stand-alone page designed for that purpose. I don’t really like the iframe approach, but it does have the benefit of being independently rendered, which gives us the opportunity to leverage the snRecordPicker for our input, which we really cannot do by simply modifying the main page directly after it has been delivered.

So let’s start out with the script that will build the HTML that we will use to replace the original text input element:

function buildHTML(prop) {
	var html = "";
	html += "<input type=\"hidden\" id=\"" + prop.property + "\" name=\"" + prop.property + "\"/>\n";
	html += "<div style=\"width: 100%; height: auto;\">\n";
	html += " <iframe id=\"frame." + prop.property + "\" src=\"/$sp.do?id=reference_properties&sysparm_property=" + prop.property + "&sysparm_table=" + prop.tableName + "&sysparm_column=" + prop.column + "&sysparm_value=" + prop.value + "\" style=\"border: none; height: 65px; width: 100%;\"></iframe>\n";
	html += "</div>\n";
	return html;
}

The hidden input element virtually replaces the original text input element, having the same name and same id. The iframe element is pretty vanilla stuff as well; the only thing of interest really is the src parameter, which points to the Portal Page that we are about to create, and passes along all of the various values needed to make the page do what we want. The Portal Page itself is just a single page with a single container filled with a single widget. The page is not really worth looking at, so let’s just jump right into the widget, as that’s where all of the magic happens. Here is the HTML:

<div id="pickerdiv">
  <sn-record-picker field="field" table="c.data.table" display-field="c.data.column" value-field="'sys_id'" search-fields="c.data.column" page-size="c.data.pageSize"></sn-record-picker>
</div>

Not much to see there, either. It’s just your standard snRecordPicker with pretty much every attribute defined by a variable. We’ll snag the values for those variables off of the URL for the page, which we populated when we constructed the src attribute for the iframe tag. The widget’s client-side script does most of the heavy lifting here:

function($scope, $location) {
	var c = this;
	var qp = $location.search();
	c.data.property = qp.sysparm_property;
	c.data.table = qp.sysparm_table;
	c.data.column = qp.sysparm_column;
	c.data.pageSize = 20;
	c.data.fieldValue = '';
	c.data.fieldDisplayValue = '';
	if (qp.sysparm_page_size) {
		c.data.pageSize = qp.sysparm_page_size;
	}
	if (qp.sysparm_value) {
		c.data.fieldValue = qp.sysparm_value;
		c.server.update().then(function(response) {
			c.data.fieldDisplayValue = response.fieldDisplayValue;
			$scope.field = {
				displayValue: c.data.fieldDisplayValue,
				value: c.data.fieldValue,
				name: 'field'
			};
		});		
	} else {
		$scope.field = {
			displayValue: '',
			value: '',
			name: 'field'
		};
	}
	$scope.$on('field.change', function(evt, parms) {
		if (parms.field.name == 'field') {
			parent.updateReferenceProperty(c.data.property, parms.field.value);
		}
	});
}

The only reason for the server-side script is to fetch the display value of the property if the property is valued at the time that the page is delivered to the browser.

(function() {
	if (input) {
		if (input.fieldValue) {
			var gr = new GlideRecord(input.table);
			gr.get(input.fieldValue);
			data.fieldDisplayValue = gr.getDisplayValue(input.column);
		} else {
			data.fieldDisplayValue = '';
		}
	}
})();

That’s about all there is to it. For every property on the page where Type=reference, the standard text input element is replaced with a hidden input element and an iframe, and inside the iframe is a ServiceNow Service Portal page that contains a single widget containing a single snRecordPicker. The parameters for the picker are passed from the iframe to the portal page via URL parameters, which are picked up by the widget and used to configure the snRecordPicker. All changes to the snRecordPicker are copied over to the hidden input field, so when the form is submitted, the selected value is sent to the server and posted to the database.

There was a minor problem with this initial version when trying to figure out the optimum height for the iframe. The height of the snRecordPicker depends on whether or not the drop-down list of choices is present, and I couldn’t find a CSS-centric way of having the iframe automatically adjust for the change in height, nor could I find a way to have the drop-down list of selectable choices overlay the content below, which is outside of the iframe. Finally, I resorted to plain old Javascript, and set up a variable called c.data.expanded to indicate whether or not the pick list was present on the screen. With a little view selection source magic, I was able to figure out that the component to watch had an id of select2-drop-mask, and so I modified the widget’s client-side code to check the required iframe height every second and adjust if needed:

function($scope, $location) {
	var c = this;
	var qp = $location.search();
	c.data.property = qp.sysparm_property;
	c.data.table = qp.sysparm_table;
	c.data.column = qp.sysparm_column;
	c.data.pageSize = 20;
	c.data.fieldValue = '';
	c.data.fieldDisplayValue = '';
	c.data.expanded = false;
	if (qp.sysparm_page_size) {
		c.data.pageSize = qp.sysparm_page_size;
	}
	if (qp.sysparm_value) {
		c.data.fieldValue = qp.sysparm_value;
		c.server.update().then(function(response) {
			c.data.fieldDisplayValue = response.fieldDisplayValue;
			$scope.field = {
				displayValue: c.data.fieldDisplayValue,
				value: c.data.fieldValue,
				name: 'field'
			};
		});		
	} else {
		$scope.field = {
			displayValue: '',
			value: '',
			name: 'field'
		};
	}
	$scope.$on('field.change', function(evt, parms) {
		if (parms.field.name == 'field') {
			parent.updateReferenceProperty(c.data.property, parms.field.value);
		}
	});
	checkHeight();
	function checkHeight() {
		var elem = document.getElementById('select2-drop-mask');
		if (elem) {
			if (elem.style.display == 'none') {
				if (c.data.expanded) {
					c.data.expanded = false;
					setHeight('65px');
				}
			} else {
				if (!c.data.expanded) {
					c.data.expanded = true;
					setHeight('300px');
				}
			}
		}
		setTimeout(checkHeight, 1000);
	}
	function setHeight(newHeight) {
		parent.updateFrameHeight(c.data.property, newHeight);
	}
}

Once that code was in place, the unexpanded state looked like this:

Modified input element with choices collapsed

… and the expanded state looked like this:

Modified input element with choices expanded

It still disturbs my sense of The Way Things Ought To Be for the left-hand edge of the revised input element not to line up with the left-hand edge of all of the other original input elements, but a fix to that was not readily apparent to me, so I have managed to let that go for now and move on to more important things. One day, though, I am going to figure out a way to fix that!

Just to recap, we modified a choice list and added an additional field to a table to provide the capability to define properties of type reference. We then created a UI Script and a Script Include so that we could replace the original input element on the property UI page, and then we created a Service Portal page and associated widget to provide the replacement for the original input element. As soon as I get a chance, I will see if I can wrap all of that up into a single Update Set and get it posted out here in case anyone wants just grab the whole package. All in all, it was a fun little project, but one that I hope to throw away one day when ServiceNow actually supports reference type properties right out of the box and having this little tweak is no longer needed.

Update: Well, it took a little longer than I had hoped to get around to this, but here is that Update Set finally.

Reference Type System Properties, Part II

“I shall either find a way or make one.”
Hannibal Barca

Last time out, we modified the sys_properties table and associated form so that we could create System Properties with a field type of Reference. This was only half the battle, however, and the easy half at that. The more difficult task will be to figure out how to get the page used to set the value of System Properties to properly render the property for user input.

Even though there is a Value field on the default System Properties form, system property values are generally set using the system_properties_ui.do page, which takes a title and one or more categories as URL parameters. You can see several examples of this in the out-of-the-box left-hand navigation menu, such as the one pictured below from the Live Feed section:

Example System Property value page

You can see from the rendered form that properties of different Types have different input mechanisms. There is a checkbox for the boolean properties and text input for the string and number properties. All we need to do is figure out how to convince it to render the Reference type properties with a selection list from the specified table. How hard can that be?

Up to this point, everything that we have done has been fairly vanilla ServiceNow stuff. Modifying Choice Lists, adding columns to Tables, manipulating Form Layouts, and creating UI Policies are all bread and butter ServiceNow sysadmin activities. There has been no need to resort to any kind of creative hackery to accomplish what we wanted do, which is always a good thing. Unfortunately, that’s all about to change.

As far as I can tell, the system_properties_ui.do page is yet another one of those items that is an integral part of the product and is not defined in the system database in a way that you can alter it in any way. As with the Email Client, we will have to rely on a script to modify the page after it has been delivered to the browser. To get an idea of how we want to do that, let’s define a property of type reference and see what the out-of-the-box functionality does with it when it renders the page. Entering sys_properties.list in the left-hand navigation search box will get us to the full list of System Properties where we can click on the New button to create our new property. At this point, I am just trying to create something that I can use to see how the input screen turns out, so I just enter Test as the Name, Test as the Description, select reference as the Type, and then select the sys_user table as the Table. Once the property has been defined, I can go back in and assign it to the Category Test by scrolling down to the bottom of the form and clicking on the New button in the Category list.

Once assigned to a Category, I can bring up the standard property value maintenance page with the URL https://<instance>.service-now.com/nav_to.do?uri=%2Fsystem_properties_ui.do%3Fsysparm_title%3DTest%2520Properties%26sysparm_category%3DTest. The good news is that the page didn’t choke on the new, unknown property type, and simply rendered the property as if the type were string:

Out-of-the-box rendering of a reference type property

More important than how it looks, though, is what’s under the hood … let’s inspect that HTML:

<tr>
 <td class="tdwrap label_left" oncontextmenu="return showPropertyContext(event, 'Test');">
  <span>
   <label class="label-inline" for="98c30fe6db362300f9699006db961935">Test</label>
   <button type="button" data-toggle="tooltip" aria-label="Property name: Test" title="" class="btn btn-icon-small btn-icon-white icon-help sn-tooltip-basic" data-original-title="Property name: Test"></button>
  </span>
 </td>
</tr>
<tr>
 <td>
  <input name="98c30fe6db362300f9699006db961935" id="98c30fe6db362300f9699006db961935" value="" aria-label="" style="width: 700px" autocomplete="off">
  <br>
  <br>
 </td>
</tr>

Pretty straightforward stuff, just a couple of single cell table rows, with the label in one row and the input box in the other. It looks like both the name and id of the input element are the sys_id of the property, so it would appear that we have everything that we need to have our way with this code, once it has been delivered to the browser.

So, here’s the plan: given the property categories available from the URL of the page, we should be able to determine all of the properties in the specified category or categories where Type=reference. Looping through that list of properties, we can find the existing input field based on the property’s sys_id, and then replace it with a more appropriate input mechanism to support the selection of records from the specified table. The only question, really, is with what will we replace the input element? If this were a Service Portal widget, we could leverage the snRecordPicker tag, but tags are useless once the page has been delivered to the browser. We could emulate everything that it does, and generate all of the HTML, CSS, and Javascript on our own, but that seems like considerably more work than I care to contemplate right now. We’ll have to give this one a little thought.

In the meantime, let’s jump on that UI Script that pulls the categories off of the URL and makes the Ajax call to the guy that will find all of the properties where Type=reference. That one shouldn’t be much work at all …

if (window.location.pathname == '/system_properties_ui.do') {
	if (window.location.href.indexOf('sysparm_category=') != -1) {
		fetchReferenceProperties();
	}
}

function fetchReferenceProperties() {
	var thisUrl = window.location.href;
	var category = thisUrl.substring(thisUrl.indexOf('sysparm_category=') + 17);
	if (category.indexOf('&') != -1) {
		category = category.substring(0, category.indexOf('&'));
	}
	if (category > '') {
		var ga = new GlideAjax('Reference_Properties');
		ga.addParam('sysparm_name', 'fetchReferenceProperties');
		ga.addParam('sysparm_category', category);
		ga.getXML(processProperties);
	}
}

function processProperties(response) {
	var answer = response.responseXML.documentElement.getAttribute("answer");
	if (answer > '') {
		var property = JSON.parse(answer);
		for (var i=0; i<property.length; i++) {
			var prop = property[i];
			var elem = gel(prop.property);
			elem.parentNode.innerHTML = buildHTML(prop);
		}
	}
}

function buildHTML(prop) {
	// we'll need to figure this out one day ...
}

Well, that wasn’t so bad. While we are thinking about the HTML that we will need to build to replace the original input tag, let’s go ahead and create that Script Include that will gather up all of the properties where Type=reference.

var Reference_Properties = Class.create();
Reference_Properties.prototype = Object.extendsObject(AbstractAjaxProcessor, {

	fetchReferenceProperties: function() {
		var property = [];
		var catList = this.getParameter('sysparm_category');
		if (catList > '') {
			catList = decodeURIComponent(catList);
			var category = catList.split(',');
			for (var i=0; i<category.length; i++) {
				var gr = new GlideRecord('sys_properties_category_m2m');
				gr.addQuery('category.name', category[i]);
				gr.addQuery('category.sys_scope', gs.getCurrentApplicationId());
				gr.addQuery('property.type', 'reference');
				gr.query();
				while (gr.next()) {
					property.push({property: gr.getValue('property')});
				}
			}
		}
		for (var i=0; i<property.length; i++) {
			var sys_id = property[i].property;
			var gr = new GlideRecord('sys_properties');
			gr.get(sys_id);
			property[i].table = gr.getValue('u_table');
			property[i].tableName = gr.getDisplayValue('u_table.name');
			property[i].value = gr.getValue('value');
			property[i].column = this.getDisplayColumn(property[i].tableName);
			property[i].displayValue = this.getDisplayValue(property[i].tableName, property[i].value, property[i].displayColumn);
		}
		return JSON.stringify(property);
	},

	getDisplayColumn: function(table) {
		if (!this.displayColumn[table]) {
			this.displayColumn[table] = this.fetchDisplayColumn(table);
		}
		return this.displayColumn[table];
	},

	fetchDisplayColumn: function(table) {
		var displayColumn = 'sys_id';
		var possibleColumn = ['name','short_description','title','description'];
		for (var i=0; i<possibleColumn.length && displayColumn == 'sys_id'; i++) {
			if (this.columnPresent(table, possibleColumn[i])) {
				displayColumn = possibleColumn[i];
			}
		}
		return displayColumn;
	},

	columnPresent: function(table, column) {
		columnPresent = false;
		var gr = new GlideRecord('sys_dictionary');
		gr.addQuery('name',  table);
		gr.addQuery('element', column);
		gr.query();
		if (gr.next()) {
			columnPresent = true;
		}
		return columnPresent;
	},

	getDisplayValue: function(table, value, column) {
		var displayValue = '';
		if (value > '') {
			var gr = new GlideRecord(table);
			gr.get(value);
			displayValue = gr.getDisplayValue(column);
		}
		return displayValue;
	},

	displayColumn: {},

	type: 'Reference_Properties'
});

This one is pretty self-explanatory, but here is the basic premise for the code: for every category in the list, we search for properties where Type=reference, and then push each one into a single pile of property objects that contain the sys_id of the property record. When we are through creating the pile, we then loop through the pile and fetch the property records using the stored sys_id so that we can add additional data to the objects, including the reference table and the current value. One of the data points that we will undoubtedly need will be the name of the field on the table that contains the “display” value. Although we could have added yet another field to the sys_properties table and had the user provide that information, for now I just hunt for it using a few potential candidates, and then fall back to sys_id if nothing else is available.

At this point, we can actually try out what we have so far, even though we still haven’t figured out what we are going to use to replace the original input element. For now, we can just dump the values onto the screen and make sure that all of the parts and pieces that we have build so far are doing what we would expect them to do. We can do that by adding a little code to the buildHTML function of our UI Script:

function buildHTML(prop) {
	return JSON.stringify(prop);
}

Now we can another little test using our previously defined test property by pulling up the same page that we did at the start.

Modified rendering of a reference type property

What that proves is that we can grab the categories from the URL, use them to find all of the reference properties in those categories, use the list of reference properties to find their corresponding input elements on the page, and then replace those input elements with something else. Now all that we have to figure out is what we really want to use to replace those input elements so that we have a pick list from the records in the specified table.

Right about now, that sounds like an interesting exercise for next time out

Reference Type System Properties

“To succeed in life, you need two things: ignorance and confidence.”
— Mark Twain

Whenever I create a ServiceNow application, I invariably end up setting up one or more System Properties to control the behavior of the app, set up options for the app, or allow the app to function differently in different environments such as Test or Production. Recently, I wanted to create a property that would be selected from a table, or in ServiceNow terms, a Reference property. Unfortunately, when I scanned the list of available property types, Reference was not on the list.

Available System Property Types

Well, not to worry. Choice Lists are easily modified, so I went to sys_properties.list, then Configure -> Table, and clicked on the Type column to bring up the Dictionary Entry. From there, I clicked on the Choices (13) tab and then clicked on the New button to create a new choice. I typed the word reference into both the Label and Value fields and then clicked on the Submit button to save the new choice. Voila! Now my instance supports System Properties of Type Reference:

New Reference Type added to the Choice List

Of course, that was the easy part … there’s still much to do. First of all, we are going to need to add another field so that we can capture the Table we will be using for the Reference. And now that I am looking at that form, there are a couple of things that are irritating my sense of The Way Things Ought To Be that I am just going to have to clean up while I’m in there. For one, the need for the Choices field is dependent on the value of the Type field, so the Type field should come first. Additionally, the Choices field shouldn’t even appear on the form if the Type is not Choice List. The new Table field, which itself should be a Reference to the Table table, should only appear if the Type is Reference. If the Type is not Choice List and the Type is not Reference, then the very next field should be Value. Let’s see if we can’t clean all of that up next.

To begin, we can add the new column to the sys_properties table right from the form itself using the hamburger menu:

Updating the Table from the Form

This will take us to the list of columns where we can click on the New button to create a new column on the table. Select Reference as the Type, enter Table as the Label and then go down to the Reference Specification tab and select Table as the Reference.

New Table Column

Saving the new column will return us to the Table specification, and clicking on the return (<) icon up in the left corner will get us back to the form where we can use the hamburger menu to select Configure -> Form Layout to move things around. Doesn’t that look better!

Rearranged System Property fields

Now, with a little UI Policy magic, we can hide both the original Choices and the new Table unless the appropriate Type has been selected. Back again to the hamburger menu where we can select Configure -> UI Policies to implement the desired changes. We’ll need to add two new policies, one for the Choices field and one for the Table field. You first have to create the policies and save them, then you can go back in and add the actions to take when the conditions are met. The condition on the first will be Type = choice list and the condition on the second will be Type = reference. Be sure the check the Reverse if false option, as we will want these policies to toggle between the true and false conditions.

Once the policies have been created, you can go back in and add the appropriate action. On the Type = choice list policy, the action will be to make the field Choices visible. On the Type = reference policy, the action will be to make the new field Table visible. Because you selected the Reverse if false option on both policies, when the the Type is not set to the specified value, the result will be that the relevant field will no longer be present on the screen.

System Property form with neither Choice List nor Reference selected

There are still a couple of more things that we could do here to make this complete, but at this point I don’t really have a pressing need for either, so I am just going to let those go for now. The first would be a potential filter for the reference fields, as there are occasions where you don’t want the user choosing from the entire contents of the table. That would basically be handled like the Table field itself, appearing on the form only when reference was selected as the Type.

The other thing that could be done at this point would be to alter the Value field to behave in accordance with the selected Type. This would seen like the appropriate thing to do, but out of the box, the open text input box does not transition to a selection of choices when you select choice list as the Type. I think the main reason that it doesn’t do that is because this is not really the place where property values are set. Properties are defined on this form, but there is another form that is generally used to set the value. We’ll tackle that form next time, as that work will be a job in and of itself.

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?