Collaboration Store, Part XLVIII

“Most times, the way isn’t clear, but you want to start anyway. It is in starting that other steps become clearer.”
Israelmore Ayivor

Last time, we created a process to retrieve the Update Set XML data from the server side and then built a UI Action to launch the installation process. At the time that we left off, I was vacillating back and forth between hacking up the original upload.do page and creating a customized copy of my own. Since that time, though, I have decided that I am much too lazy to try to build one of my own, so I am just going to attempt to hack up the one that already exists with as minimal amount of intervention as I can muster. The one way that I know how to do that is to create a global UI Script that modifies the page on the fly without actually altering the source of the page itself. We have already used this technique with our earlier incident email hack, so at least we know that this approach is one that will work.

Unfortunately, you cannot create global UI Scripts in a Scoped Application; the script has to be in the global scope, so this component will be yet another addition to our global components Update Set. I don’t really like having all of these parts outside of the application, but that’s just the way that these things go sometimes. These global scripts run on every single page load in the system, so to be a minimally intrusive as possible, the very first thing that you want to check is whether or not you are running on a page in which this code is needed. For our purposes, we only want this code to run on the upload.do page, and only if our attachment_id parameter is present in the URL.

if (window.location.pathname == '/upload.do' && window.location.search.startsWith('?attachment_id=')) {
	alert('So far, so good ...');
}

We can test this out by going into a version record and clicking on the new Install form button.

First test of the new global UI Script

OK, that works. In fact, that also proves out the code on the UI Action that we created last time. As the alert says, so far, so good. One thing that you will notice, however, is that there is nothing on the underlying screen. This code runs as soon as it is loaded, and the rest of the page has yet to be delivered. Since our plan is to tinker with that page, we really don’t want our code to be running just yet. We will need to wait to make sure that the rest of the page is there as well before we attempt to alter it. We can accomplish that with a little recursive loop that will look for an important field such as the file to be uploaded, and until that element is present, just loop back and check again. Here is a modified version of the script that will accomplish that.

if (window.location.pathname == '/upload.do' && window.location.search.startsWith('?attachment_id=')) {
	waitForPageLoad();
}

function waitForPageLoad() {
	if (document.getElementById('attachFile')) {
		installApplication();
	} else {
		setTimeout(waitForPageLoad, 100);
	}
}

function installApplication() {
	alert('So far, so good ...');
}

If that works as intended, the alert should not pop until at least the parts of the page in which we are interested have arrived.

Second test of the new global UI Script

That’s better. Now at least the stuff that we want to play with is all present in the DOM. The first thing that we will want to do is to hide the original form and then replace it with some kind of message indicating that things are happening in the background and there is nothing for the operator to do right at the moment. Here is a little code that will find the DIV that contains the major components, hides it, and replaces it with something else.

var originalContent = document.getElementsByClassName('section-content')[0];
originalContent.style.visibility = 'hidden';
var newContent = document.createElement('div');
newContent.innerHTML = '<h4 style="padding: 30px;">&nbsp;<img src="/images/loading_anim4.gif" height="18" width="18">&nbsp;Uploading Update Set XML file ...</h4>';
originalContent.parentNode.insertBefore(newContent, originalContent);

There are a couple of things to note on the above code. For one, DOM manipulation is frowned upon in the ServiceNow environment. You will get tagged for that in an Instance Scan as a bad practice, and you should really try to avoid doing things like that if at all possible. Still, sometimes you have to break the rules to get something done; there is a reason that this site is called ServiceNow Hackery and not ServiceNow By The Book. Sometimes you have to step outside of the lines in order to do what you want to do. But again, this should be a last resort and not adopted as a routine way of doing things. The other thing to note is the use of the innerHTML method. Again, the preferred way of doing things would be to create each DOM node individually, set all of the appropriate values on each node, and then link them all up to each other before inserting them into the active DOM. That’s the way that it should be done, but I was just too lazy to go through all of that and I took the easy way out instead. But that’s another thing to which folks might take exception in certain circles.

To test all of this out, we can go back to our version page and click on the new Install button one more time.

Third test of the new global UI Script

With all of that basic housekeeping out of the way, we can now focus on what we are here for. The first thing that we need to do in order to accomplish our goal is to pull down the Update Set details using GlideAjax to access the Script Include that we created last time. Before we do that, though, we need to snag the attachment record sys_id from the URL parameter. With that in hand, we can then make our Ajax call.

var attachmentId = window.location.search.substring(15);
var ga = new GlideAjax('x_11556_col_store.ApplicationInstaller');
ga.addParam('sysparm_name', 'getXML');
ga.addParam('attachment_id', attachmentId);
ga.getXMLAnswer(submitForm);

Now we just need to build a submitForm function that will parse the returned JSON string to access the file name and file contents, and then somehow use that as if it were a file on the local system so that we can submit the form. That sounds like a bit of work in and of itself, and I’m still not exactly sure how I am going to pull that off, so let’s save that exercise for our next exciting installment.

Incident Email Hack Revisited

“The greatest performance improvement of all is when a system goes from not working to working.”
John Ousterhout

The other day I was showing off my Incident email hack, and to my surprise, the thing did not work. I was reminded of something my old boss used to tell me whenever we had blown a demonstration to a potential customer. “There are only two kinds of demos,” he would say, “Those that don’t count and those that don’t work.” But my email hack had been working flawlessly for quite some time, so I couldn’t imagine why it wasn’t working that day. Then I realized that I couldn’t remember trying it since I upgraded my instance to Madrid. Something was different now, and I needed to figure out what that was.

It didn’t take much of an investigation to locate the failing line of code. As it turns out, it wasn’t in anything that I had written, but in an existing function that I had leveraged to populate the selected email addresses. That’s not to suggest that the source of the problem was not my fault; it just meant that I had to do a little more digging to get down to the heart of the issue. The function, addEmailAddressToList, required an INPUT element as one of the arguments, but for my usage, there was no such INPUT element. But when I looked at the code inside the function, the only reference to the INPUT element was to access the value property. So, I just created a simple object and set the value to the email address that I wanted to add, and then passed that in to the function. That worked just fine at the time, but that was the old version of this function.

In the updated version that comes with Madrid, there is new code to access the ac property of the INPUT element and run a function of the ac object called getAddressFilterIds. My little fake INPUT element had no ac property, and thus, no getAddressFilterIds function, so that’s where things broke down. No problem, though. If I can make a fake INPUT element, I can add a fake ac object to it, and give that fake ac object a fake getAddressFilterIds function. I would need to know what the function does, or more importantly, what it is supposed to return, but that was easy enough to figure out as well. In the end, all I really needed to do to get past that error was add these lines to the incident_email_client_hack UI Script:

input.ac = {};
input.ac.getAddressFilterIds = function() {return '';};

Unfortunately, things still didn’t work after that. Once I got past that first error, I ran right into another similar error, as it was trying to run yet another missing function of the ac object called resetInputField. So, I added yet another line of code:

input.ac.resetInputField = function() {return '';};

Voila! Now we were back in action. I did a little more testing, just to be sure, but as far as I can tell, that solved the issue for this version, and since all I did was add bloat to the fake INPUT element that would never be referenced in the old version, it would be backwards compatible as well, and work just fine now in either version. Still, now that all the parts were laid out, I decided that I could clean the whole mess up a little bit by defining the fake INPUT element all in a single line of code:

var input = {
	value: recipient[i].email,
	ac: {
		getAddressFilterIds: function() {
			return '';
		},
		resetInputField: function() {
			return '';
		}
	}
};

There, that’s better! Now, instead of adding three new lines of code, I actually ended up removing a line. For those of you playing along at home, I gathered up all of the original parts and pieces along with the updated version of this script and uploaded a new version of the Update Set for this hack.

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