Refactoring the SNH Data Table Widget, Part IV

“Testing is an infinite process of comparing the invisible to the ambiguous in order to avoid the unthinkable happening to the anonymous.”
James Bach

Last time, we tested a number of the features of the refactored SNH Data Table collection, but there is still much to do before we can bundle this all up into a new Update Set and stuff it out on Share. Now that we know that the initial version that I put out there is missing a critical component, I’d like to wrap this up and replace it with the refactored version, so let’s get to it.

To test the bulk actions and other clicks that do not result in navigating to a new portal page, I took my original Button Click Handler Example widget, renamed it the Table Click Handler Example widget, and then reworked it to handle all four of the clickable features, reference links, aggregate columns, buttons and icons, and bulk actions. Here is the new Client script for the repurposed widget:

function (spModal, $rootScope) {
	var c = this;
	var eventNames = {
		referenceClick: 'data_table.referenceClick',
		aggregateClick: 'data_table.aggregateClick',
		buttonClick: 'data_table.buttonClick',
		bulkAction: 'data_table.bulkAction'
	};

	$rootScope.$on(eventNames.referenceClick, function(e, parms) {
		displayClickDetails(eventNames.referenceClick, parms);
	});

	$rootScope.$on(eventNames.aggregateClick, function(e, parms) {
		displayClickDetails(eventNames.aggregateClick, parms);
	});

	$rootScope.$on(eventNames.buttonClick, function(e, parms) {
		displayClickDetails(eventNames.buttonClick, parms);
	});

	$rootScope.$on(eventNames.bulkAction, function(e, parms) {
		displayClickDetails(eventNames.bulkAction, parms);
	});

	function displayClickDetails(eventName, parms) {
		var html = '<div>'; 
		html += ' <table>\n';
		html += '  <tbody>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Event: &nbsp;</td>\n';
		html += '    <td>' + eventName + '</td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Table: &nbsp;</td>\n';
		html += '    <td>' + parms.table + '</td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Sys ID: &nbsp;</td>\n';
		html += '    <td>' + parms.sys_id + '</td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Config: &nbsp;</td>\n';
		html += '    <td><pre>' + JSON.stringify(parms.config, null, 4) + '</pre></td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Item(s): &nbsp;</td>\n';
		html += '    <td><pre>' + JSON.stringify(parms.record, null, 4) + '</pre></td>\n';
		html += '   </tr>\n';
		html += '  </tbody>\n';
		html += ' </table>\n';
		html += '</div>';
		spModal.alert(html);
	}
}

This will allow me to test a number of things, all with the same companion widget, which should save a little bit of time. The original button_test page has a number of options in the configuration, so let’s pull that guy up and click around and see what we can find out.

Original button_test page

One thing that I noticed right away was that the master checkbox was not working. It seems that when I rebuilt the core SNH Data Table widget from the latest version of the stock Data Table widget, I neglected to paste back in the following added function:

$scope.masterCheckBoxClick = function() {
	for (var i in c.data.list) {
		c.data.list[i].selected = c.data.master_checkbox;
	}
};

Putting that back solved that problem, but then I also discovered that I needed to add spModal to the function arguments so that the error message would come up when you tried to select a bulk action from the drop-down without selecting any of the records in the table. Once I got all that out of the way, I got back to testing the buttons and icons.

Clicking on the Status Check icon

So now when I click on a button or icon, the alert pops up with all of the information that is passed with the broadcast message, which gives you an idea of what you have to work with in your companion widget to take whatever action that you would like to take based on that information. We should be able to do the same thing when selecting one or more rows and then choosing a bulk action.

Selecting a bulk action

Looks like that is working as well, so I think things are looking pretty good at this point. There is another old test page that we can pull called snh_data_table that has a companion widget for one of the icons.

snh_data_table test page

Once again, we will need to update the Client script in the click handler widget to adapt to our new approach to click handling.

function(spModal, $rootScope) {
	var c = this;
	var eventNames = {
		referenceClick: 'data_table.referenceClick',
		aggregateClick: 'data_table.aggregateClick',
		buttonClick: 'data_table.buttonClick',
		bulkAction: 'data_table.bulkAction'
	};

	$rootScope.$on(eventNames.buttonClick, function(e, parms) {
		if (parms.config.name == 'icon') {
			spModal.open({widget: 'snh-user-profile', widgetInput: {sys_id: parms.sys_id}}).then(function() {
				//
			});
		}
	});
}

Since the button is configured to launch a new page and the icon is configured to pop up a modal dialog, we need to check to make sure that the button click is for the icon and not the button. Other than that, it is very similar to the other examples.

Clicking on the icon to bring up the modal dialog

So that works. Very nice. Obviously, there is a lot more that we could do here to check out every little thing, but I think that we have covered most of the high points, and given that the version that is out on Share right now contains a pretty significant flaw, I think I would like to roll the dice and toss this out there as is to resolve that issue. Hopefully, I will not miss any important artifacts this time! Here is the Update Set for those of you who would like to check it out. As always, please feel free to leave any feedback of any kind in the comments. Thanks!

Fun with Outbound REST Events, Part VI

“Quality is never an accident. It is always the result of intelligent effort.”
John Ruskin

Now that we have completed our address verification feature, we can finally turn our full and complete attention to the actual purpose of this entire adventure, which which is to explore the use of ServiceNow Event Management practices on the internal workings of ServiceNow itself. When we last left our Script Include, we had identified a number of places in the script where things could potentially go wrong. As a temporary measure, we just put a simple gs.info statement in each one of those places. Now we want to replace those with Event logging so that we can leverage the built-in power of the ServiceNow Event Management infrastructure.

To make that easier, we built a utility a while back to handle much of the heavy lifting of logging an Event. We can take advantage of that utility and minimize the code that we will need to our Script Include. Each gs.info statement will need to be replaced with something like this:

var seu = new ServerEventUtil();
seu.logEvent(source, resource, metric_name, severity, description, additional_info);

Now we just need to figure out what values to send for each of those function arguments. Let’s take them one at a time.

source

This is the source of the Event, which is our case is the Script Include that is logging the Event. Since the name of the Script Include is always stored in an internal property called type, I just like to pass this.type for this argument, which works in all Script Includes without modification.

resource

This is a reference to thing that you were working on when the problem occurred. In our case, this would be a User, but in the current configuration, we do not have a handle on the User record that is being updated. We could use the address here, just to have some kind of unique value, but when we turn this Event into an Incident, it would be good to know which User was being updated. The solution to that would be to have the calling script pass some reference to User record as an additional argument to the function. That’s a little more work, but it will be worth it in the long run.

metric_name

This is basically the problem that occurred, and we will end up with a different value here for different issues such as an unparsable JSON string or a bad HTTP Response Code.

severity

This is just your standard severity values, and for our purposes, I think we will just pass a hard-coded 3 (Moderate) here.

description

As the name implies, this is just a text description of what happened. Ours will be unique to the problem that occurred.

additional_info

This is an open-ended JSON object into which you can stuff basically anything that you might want to know about what happened that isn’t already in a defined property. The Event logging utility automatically adds some standard things to this object such as user information and a stack trace, but we will want to add some additional information as well such as what was sent to the service and what came back. It takes a bit of code to construct the additional info object, so I like to build a function for that so that it can be called from wherever it is needed instead of duplicating the code everywhere. Here is the one that we will add for this exercise:

buildAdditionalInfo: function(input, response, respObject, exception) {
	var additionalInfo = {input: {}, response: {}};

	additionalInfo.input.street = input.street;
	additionalInfo.input.city = input.city;
	additionalInfo.input.state = input.state;
	additionalInfo.input.zip = input.zip;
	additionalInfo.response.code = response.getStatusCode();
	additionalInfo.response.content = response.getBody();
	additionalInfo.response.headers = response.getHeaders();
	if (respObject) {
		additionalInfo.response.object = respObject;
	}
	if (exception) {
		additionalInfo.exception = exception.toString();
		additionalInfo.stackTrace = exception.stack;
	}

	return additionalInfo;
},

Using a function for this not only consolidates the code into a single place, it also ensures some consistency between the various Events, which makes it easier to pull the data back out when you want to use it for things like formatting the description of a resulting Incident.

Now that know how we are going to populate these arguments, let’s go down through the code and replace each of our gs.info statements with Event logging. The first one that we come across is the JSON parsing exception.

try {
	respArray = JSON.parse(body);
} catch (e) {
	seu.logEvent(
		this.type,
		user,
		'Unparsable response',
		3,
		'The response content received from the US Address validation service could not be parsed.',
		this.buildAdditionalInfo(response, resp, null, e));
}

At this point in the process, we do not have a response object, but we do have an exception, so we pass null as the response object to the function that builds out the additional info. All of the others will be very similar, so we don’t have to go through each one individually. Here is the complete function, with all of the gs.info statements replaced and the user identifier added as a function argument:

validateAddress: function(user, street, city, state, zip) {
	var response = {result: 'failure', street: street, city: city, state: state, zip: zip};

	var seu = new ServerEventUtil();
	var rest = new RESTMessage('US Street Address API', 'get');
	rest.setStringParameter('authid', gs.getProperty('us.address.service.auth.id'));
	rest.setStringParameter('authToken', gs.getProperty('us.address.service.auth.token'));
	rest.setStringParameter('street', encodeURIComponent(street));
	rest.setStringParameter('city', encodeURIComponent(city));
	rest.setStringParameter('state', encodeURIComponent(state));
	rest.setStringParameter('zip', encodeURIComponent(zip));
	var resp = rest.execute();
	var body = resp.getBody();
	if (resp.getStatusCode() == 200) {
		var eventLogged = false;
		var respArray = [];
		try {
			respArray = JSON.parse(body);
		} catch (e) {
			seu.logEvent(
				this.type,
				user,
				'Unparsable response',
				3,
				'The response content received from the US Address validation service could not be parsed.',
				this.buildAdditionalInfo(response, resp, null, e));
			eventLogged = true;
		}
		if (respArray && respArray.length > 0) {
			respObj = respArray[0];
			if (typeof respObj.analysis == 'object') {
				var validity = respObj.analysis.dpv_match_code;
				if (validity == 'Y' || validity == 'S' || validity == 'D') {
					response.result = 'valid';
					response.street = respObj.delivery_line_1;
					response.city = respObj.components.city_name;
					response.state = respObj.components.state_abbreviation;
					response.zip = respObj.components.zipcode;
					if (respObj.components.plus4_code) {
						response.zip += '-' + respObj.components.plus4_code;
					}
				} else {
					response.result = 'invalid';
				}
			} else {
				seu.logEvent(
					this.type,
					user,
					'Invalid Response Object',
					3,
					'The response object received from the US Address validation service was not valid.',
					this.buildAdditionalInfo(response, resp, respObj));
			}
		} else {
			if (!eventLogged) {
				seu.logEvent(
					this.type,
					user,
					'Invalid Response Content',
					3,
					'The response content received from the US Address validation service was not valid.',
					this.buildAdditionalInfo(response, resp));
			}
		}
	} else {
		seu.logEvent(
			this.type,
			user,
			'Invalid Response Code',
			3,
			'The response code received from the US Address validation service was not valid.',
			this.buildAdditionalInfo(response, resp));
	}

	return response;
},

The one place where we had to add a little bit of extra logic was the Event that is triggered when the respArray is empty. One possible reason for that field to be empty would be if we failed to successfully parse the JSON string. When that happens, we have already logged an Event, so we would not want to now log a second one for the same issue. To prevent that from happening, we added the eventLogged variable, and then we only log an Event later on if that variable is still set to false. Other than that one special circumstance, all of these are pretty much the same other than the unique values that are specific to the particular problem triggering the Event.

That completes the modifications necessary to support Event logging, but since we added the user identifier to the list of function arguments, we still have a little work to do to carry that change forward through all of the other components. To begin, we will have to collect the user value from the Ajax parameters and pass that on to the primary function. That client callable function now looks like this:

validateAddressViaClient: function() {
	var user = this.getParameter('sysparm_user');
	var street = this.getParameter('sysparm_street');
	var city = this.getParameter('sysparm_city');
	var state = this.getParameter('sysparm_state');
	var zip = this.getParameter('sysparm_zip');
	return JSON.stringify(this.validateAddress(user, street, city, state, zip));
},

Not much change here; we pull one more parameter into a variable and then add that variable to the function call arguments. Of course, none of that will do any good if we don’t send that extra parameter with the Ajax call, so we will need to modify our Client Script as well. Again, there is not much to change here, but we need to make the change. Our code to value the parameters now has one additional line:

ga.addParam('sysparm_name', 'validateAddressViaClient');
ga.addParam('sysparm_user', g_form.getValue('user_name'));
ga.addParam('sysparm_street', street);
ga.addParam('sysparm_city', city);
ga.addParam('sysparm_state', state);
ga.addParam('sysparm_zip', zip);

That completes the changes that we need to make in order to log an Event whenever something unexpected occurs. We still need to test everything to make sure that it all works, but to do that, we are going to have to force some kind of error to occur. That sounds like a project in and of itself, so this seems like a good stopping place for now. We’ll figure out all of that testing stuff in our next installment.

Fun with Outbound REST Events, Part V

“The best preparation for good work tomorrow is to do good work today.”
Elbert Hubbard

At the end of the last installment in this series, I mentioned two possible options for the next item on which to focus our energies. At the time, I wasn’t really sure which direction would be the preferable choice, but now that it is time to fish or cut bait, a decision needs to be made. Given that the entire purpose of this exercise is to demonstrate the use of Event Management practices on internal ServiceNow processes, I believe it would be good to go ahead and wrap up our example Use Case at this point so that we can devote the remainder of our time exclusively on the Event Management aspects. All that is really left to do in order to to complete our address verification scenario is to add the form validation to the User Profile form. That process will leverage the new Script Include and Outbound REST Message that we have already completed.

There are two different ways that we can go about this: we can use an onSubmit Business Rule on the server side, or we can use an onSubmit Client Script on the client side. The Business Rule route is actually a little easier, as you have access to both the current and previous versions of the GlideRecord, and you can call the Script Include directly, without the need for GlideAjax. My problem with that, though, from a User Experience perspective, is that you have to submit the entire form to the server for processing, which then gets reloaded if you have validation issues. My preference is to validate the form right where it sits on the client side, before the form ever gets submitted to the server. For that, we need to build a Client Script.

In fact, for our purpose, we will need two Client Scripts, one an onLoad script and the other an onSubmit script. The reason that we have to have an onLoad script is because you do not have access to the previous field values on the client side like you do with a server-side Business Rule. We will need to snag those values and stuff them somewhere for safekeeping as soon as the form loads. The ServiceNow platform provides a place for just that sort of thing called the g_scratchpad. You can pretty much throw whatever you want in there and it will be available for use until the form is reloaded. The entire onLoad script is just a few short lines of code.

function onLoad() {
	g_scratchpad.originalStreet = g_form.getValue('street');
	g_scratchpad.originalCity = g_form.getValue('city');
	g_scratchpad.originalState = g_form.getValue('state');
	g_scratchpad.originalZip = g_form.getValue('zip');
}

That’s all there is to that. With those initial values safely tucked away, we can then pull them back out later on and refer to them in our onSubmit script to see if anything has changed. Before we start in on our onSubmit script, however, we need to go back to our Script Include and add just a bit of code. When we first built out Script Include, we did not set it up to be client callable, but we are going to need to do that now so that we can access it via GlideAjax. It’s a simple checkbox on the Script Include form, which triggers a slight change in the prototype for the script. That change is handled automatically for any new script, but since we have been working on ours for a while, checking the box may not actually alter any modified code. In that case, you just need to modify the prototype line yourself to look like this:

AddressValidationUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {

Also, you will need to go down to the very bottom of the script and insert a closing paren in between the final curly brace and the terminating semi-colon. Once that’s done, we can add a new function that we can call from the client side.

validateAddressViaClient: function() {
	var street = this.getParameter('sysparm_street');
	var city = this.getParameter('sysparm_city');
	var state = this.getParameter('sysparm_state');
	var zip = this.getParameter('sysparm_zip');
	return JSON.stringify(this.validateAddress(street, city, state, zip));
},

This function just grabs all of the parameters passed via GlideAjax and passes them to our existing function, and then turns the response object into a string that can be returned in the XML Ajax response. Now we are all set up to receive calls from the client side.

Being a validation script, our onSubmit script will need to prevent the submission of the form in the event that any validation errors are detected. Without an Ajax call back to the server, that’s easily accomplished by returning false from the onSubmit function. However, making an asynchronous Ajax call means you are leaving your function without the answer in hand, so you don’t know whether or not any issues were detected until the response comes back, which will be in a completely different thread. Although you could utilize the getXMLWait() method to simply wait for the answer, that approach is quite frowned upon in Client Scripts for a variety of reasons, so we do not want to go down that road. Instead, we will use a modified version of this technique.

The approach is to cancel submission of the form before making the Ajax call, and then when the response comes in, submit the form a second time if all is well. Of course, submitting the form again will launch the onSubmit script again, so we need to make it smart enough to know that this is the second submit and to not start the whole process all over again. To accomplish that, we use yet another g_scratchpad property to let the script know that validation has already taken place. Here is the main onSubmit script:

function onSubmit() {
	var submitForm = true;

	var actionName = g_form.getActionName();
	if (!g_scratchpad.isFormValid) {
		submitForm = checkAddress();
	}

	return submitForm;
}

On the first submit, we default the submitForm variable to true, capture the name of the button that was pushed to submit the form so that we can use it when we submit again later, and then check to see if validation has already taken place and passed by testing the isFormValid scratchpad property. In the case of the first submit, isFormValid has not been set to true, so we check the address to see if it needs validated. If it does, then the submitForm variable will be set to false, and the form will not be submitted.

Assuming that it does need to be validated, the checkAddress function will make the Ajax call and cancel the original form submission by returning false. When the Ajax response comes in, if the address is valid, the response function will then submit the form a second time using the saved actionName after setting the isFormValid scratchpad property to true. When the onSubmit function then starts again due to the second submit, it will bypass the address check and simply allow the form to submit due to the isFormValid scratchpad property being set to true.

It’s all a little convoluted, but it works. Here is the checkAddress function:

function checkAddress() {
	var submitForm = true;

	var street = g_form.getValue('street');
	var city = g_form.getValue('city');
	var state = g_form.getValue('state');
	var zip = g_form.getValue('zip');
	if (street || city || state || zip) {
		if (street != g_scratchpad.originalStreet || city != g_scratchpad.originalCity || state != g_scratchpad.originalState || zip != g_scratchpad.originalZip) {
			GlideUI.get().clearOutputMessages();
			var ga = new GlideAjax("AddressValidationUtils");
			ga.addParam('sysparm_name', 'validateAddressViaClient');
			ga.addParam('sysparm_street', street);
			ga.addParam('sysparm_city', city);
			ga.addParam('sysparm_state', state);
			ga.addParam('sysparm_zip', zip);
			ga.getXMLAnswer(processXMLAnswer);
			submitForm = false;
		}
	}

	return submitForm;
}

Just like in the main onSubmit function, we first default the submitForm variable to true, and then we grab all of values for the four address components off of the g_form object. The first thing that we check is if there is even any data in any of the four fields. If they are all empty, then there is nothing to validate. If there is some data there, then the next thing that we check is if it is any different than the values that we squirreled away in our onLoad script. If there are no changes, then again there is no need for validation. But if there is data there and it has changed in any way, now we are going to be making that GlideAjax call. Most of that is pretty standard GlideAjax stuff, but we also clear out any error messages on the screen from previous attempts and set the submitForm variable to false to kill the original form submission.

To process the Ajax response, we have yet another function, processXMLAnswer:

function processXMLAnswer(answer) {
	var response = JSON.parse(answer);
	if (response.result == 'invalid') {
		g_form.addErrorMessage('Unable to verify address entered using the US Address validation service');
		g_form.showFieldMsg('street', 'Unverifiable address', 'error');
		g_form.showFieldMsg('city', 'Unverifiable address', 'error');
		g_form.showFieldMsg('state', 'Unverifiable address', 'error');
		g_form.showFieldMsg('zip', 'Unverifiable address', 'error');
	} else {
		if (response.result == 'valid') {
			g_form.setValue('street', response.street);
			g_form.setValue('city', response.city);
			g_form.setValue('state', response.state);
			g_form.setValue('zip', response.zip);
		}
		g_scratchpad.isFormValid = true;
		g_form.submit(actionName);
	}
}

The response comes back in the form of a string, so the first thing that we have to do is convert it back to an object. If the result property of that object is ‘invalid’, then we leave the form unsubmitted and throw a few error messages up on the screen; otherwise, we are going to submit the form a second time. But before we do that, we check to see if the result property is ‘valid’, in which case we overlay the user’s input with the clean address values returned by the service. After that , we set our isFormValid scratchpad property to true and resubmit the form using the saved actionName.

It’s all a little complicated, having to pass through the onSubmit script twice due to the double submit, but it all makes sense if you really think about it. Of course, we won’t really know if it works until we try it, so let’s pull up a user’s profile and give it a shot.

For our first test, let’s see if we can get a validation error. We can use the test address that we have been using up to this point, but let’s change the state from FL to TX and let’s drop the zip code entirely. Also, let’s put everything in lower case, just for fun. OK, let’s see what happens.

Test using invalid address

Having both form-level and field-level error messages is probably a little overkill, but everything seems to have worked. Now, let’s change the state back to FL and see if that is enough to consider it a valid address.

User profile after address validation

Not only did it pass validation, it also corrected our capitalization and added the missing zip code. That’s actually pretty slick. This wasn’t really the point of this series, but I like this address validation feature. I was originally just looking for something that might have a failure to showcase the Event Management stuff, but this turned out to be a pretty handy little addition that could be useful in a number of other places, such as the Building and Location forms.

This now completes the example working feature that has the potential for failure. From this point on, we can focus on what we came here for, which is to log and process Events that originate in ServiceNow. Next time out, we will start in on the logging.

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.