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.

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.