Service Portal User Directory, Part II

“Twenty years from now you will be more disappointed by the things that you didn’t do than by the ones you did do.”
Mark Twain

I did not really intend for this to be a multi-part exercise, but I ran into a little problem last time and so I needed a little time to come up with a solution. The problem, you may recall, is that I changed the destination page on the table widget options to the user_profile page, and now clicking on a department or location brings you to that page, where it cannot find a user with that sys_id. We definitely have a way around that by using the built-in reference page map to map a different page to references from those tables, but the question is, where do we want to send regular users who click on those links? I know I do not want to send them to the form page, so I went looking for some other existing page focused on departments or locations. Not finding anything to my liking, I resigned myself to the fact that I was going to have to come up with something on my own, and started thinking about what it is that I would like to see.

Since this is User Directory, I decided that what would really be interesting, and potentially useful, would be a list of Users assigned to the department or location. That seemed like a simple thing to do with my new SNH Data Table from JSON Configuration, and with just a little bit of hackery, I think I could create one configuration script that would work for both departments and locations. This time, instead of starting with the page layout, I decided to start with that multi-purpose configuration script. Here’s the idea: create a new script called RosterConfig that has just one Perspective (All), and then use the State options as indicators of which entity we want (department or location). Here is what I came up with using the Content Selector Configuration Editor:

var RosterConfig = Class.create();
RosterConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'all',
		label: 'All',
		roles: ''
	}],

	state: [{
		name: 'department',
		label: 'Department'
	},{
		name: 'location',
		label: 'Location'
	}],

	table: {
		all: [{
			name: 'sys_user',
			displayName: 'User',
			department: {
				filter: 'active=true^department=javascript:$sp.getParameter("sys_id")',
				fields: 'name,title,email,location',
				btnarray: [],
				refmap: {
					cmn_location: 'location_roster'
				},
				actarray: []
			},
			location: {
				filter: 'active=true^location=javascript:$sp.getParameter("sys_id")',
				fields: 'name,title,department,email',
				btnarray: [],
				refmap: {
					cmn_department: 'department_roster'
				},
				actarray: []
			}
		}]
	},

	type: 'RosterConfig'
});

My plan was to create a department_roster page and a location_roster page, so I mapped the cmn_location table to the location_roster page in the department state and cmn_department table to the department_roster page in the location state. Then I went ahead and built the department_roster page, pulled it up in the Service Portal Designer, and dragged the SNH Data Table from JSON Configuration widget into a full-width container. Using the little pencil icon to bring up the widget options editor for the widget, I entered the name of our new configuration script and set the State to department.

Configuring the custom Data Table widget

I essentially repeated the same process for the location_roster page, but for that page, I set the State to location. Now all that was left to do was to go back into the UserDirectoryConfig script and map the department and location tables to their respective new pages. But before I did that, I wanted to test things out, just to make sure that everything was working as I had intended. Unfortunately, that was not the case. It turns out that my attempt to snag the sys_id off of the URL in the filter was not working. Presumably, the embedded script doesn’t work because the script engine that runs the code does not have access to $sp:

active=true^department=javascript:$sp.getParameter("sys_id")

So, I tried a few more things:

gs.action.getGlideURI().get("sys_id")
RP.getParameterValue("sys_id")
$location.search()["sys_id"]
(... and too many others to list here!)

The bottom line for all of that was that nothing worked. As far as I can tell, by the time that you are running the script in the filter to obtain the value, you have lost touch with anything that might have some kind of relationship with the current URL. So I gave up on the idea of running a script and switched my filter to this:

active=true^department={{sys_id}}

Of course, that doesn’t do anything, either. At least, it didn’t until I added the following lines to my base Data Table widget right after I obtained the filter from its original source:

if (data.filter && data.filter.indexOf('{{sys_id}}')) {
	data.filter = data.filter.replace('{{sys_id}}', $sp.getParameter('sys_id'));
}

I don’t really like doing that, as it is definitely a specialized hack just for this particular circumstance, but it does work, so there is that. The one consolation that I could think of was that sys_id was probably the only thing that I would ever want to snag off of the URL, and I might find some other context in which I might want to do that again, so it was not that use-case specific. Still, I would have preferred to have gotten this to work without having to resort to that.

Once I got over that little hurdle, I decided that I really did not like the page being just the list of users. I wanted to have some kind of context at the top of the page, so I ended up building another little custom widget to sit on top of the data table. Here is the HTML that I came up with for that guy:

<div ng-hide="data.name">
  <div class="alert alert-danger">
    ${Department not found.}
  </div>
</div>
<div ng-show="data.name">
  <div style="text-align: center;">
    <h3 class="text-primary">{{data.name}} ${Department}</h3>
  </div>		
  <div>
    <p>{{data.description}}</p>
    <p>
      <span style="font-weight: bold">${Department Head}</span>
      <br/>
      <span ng-hide="data.dept_head_id"><i>(Vacant)</i></span>
      <span ng-show="data.dept_head_id">
        <sn-avatar primary="data.dept_head_id" class="avatar-smallx" show-presence="true" enable-context-menu="false"></sn-avatar>
        <span style="font-size: medium;">{{data.dept_head}}</span>
      </span>
  </div>		
</div>		

… and here is the server side script:

(function() {
	var deptGR = new GlideRecord('cmn_department');
	if (deptGR.get($sp.getParameter('sys_id'))) {
		data.name = deptGR.getDisplayValue('name');
		data.description = deptGR.getDisplayValue('description');
		data.dept_head = deptGR.getDisplayValue('dept_head');
		data.dept_head_id = deptGR.getValue('dept_head');
	}
})();

Now it looks a little better:

Final Department Roster page

I something similar for the location_roster page, and after that, all that was left was to go back into the original UserDirectoryConfig script and map the department and location tables to their new pages.

Mapping the tables to their respective pages

With that out of the way, now you can bring up the directory, click on a location, click on a department in the location roster, and then click on a user, and since every page includes the dynamic breadcrumbs widget, it all gets tracked at the top of the page.

User Profile Pa

I ended up having to do a little more work than I had originally anticipated with the need to build out the custom department_roster and location_roster pages, but that gave me a chance to utilize my newest customization of the Data Table widget, so it all worked out in the end. If you would like to play around with it on your own instance, here is an Update Set that should contain all of the parts that you need.

Service Portal User Directory

“Do. Or do not. There is no try.”
Yoda

I have been playing around a lot lately with my hacked up Service Portal Data Table widget, but just about everything that I have done has been centered around some kind of Task. I have been wanting to do something a little different, so I decided that I would use my breadcrumbs widget, my content selector, and my custom data table to create a User Directory portal page. I started out by creating a new page called user_directory and opening it up in the Portal Designer.

First I dragged a 12-wide container over into the top of the page and then dragged the breadcrumbs widget into that. Then I dragged over a 3/9 container and put the content selector into the narrow portion and the data table into the wider portion. That gave me my basic layout.

user_directory page basic layout

Since this was going to be a list of Users on the portal, I decided that the page to which we will link should be the user_profile portal page, so I updated the table options to link to that page, and to add a user icon as the glyph.

Updating the data table widget options

We also need to update the content selector widget options, but it is going to ask for a configuration script, which we have not built just yet, so let’s do that now. For this, we can use the new Content Selector Configuration Editor to create a new configuration script called UserDirectoryConfig.

Creating a new configuration script using the Content Selector Configuration Editor

For this initial version of the directory, we will have two Perspectives: 1) a general public perspective for all authenticated users, and 2) a user administration perspective for those with the authority to update user records. We will also define 3 States: 1) active users, 2) inactive users, and 3) all users. For each Perspective, we will define a single Table, the User (sys_user) table.

Updating the new configuration script using the Content Selector Configuration Editor

For all of the public states, we will specify the following field list:

name,title,department,location,manager

Since I do not want regular users looking at inactive users, I used the following filter for both the Active state and the All state:

active=true

For the Inactive state, which I wanted to always come up empty, I used this:

active=true^active=false

For the admin states, we will drop the manager and add the user_name, and for the All state, we will also add the active flag:

user_name,name,title,department,location,active

And of course the filters for the admin states are all pretty self explanatory, so there’s no need to waste any space on those here. One little extra thing that I did want to add to all of the admin states, though, was an Edit button. Clicking on the user_name will get you to the user_profile page, but if you want to edit the user information, you need to go the form page, so I added a button for that purpose to all three of the admin states.

Administrative Edit button

After completing the configuration, saving the form produced the following configuration script:

var UserDirectoryConfig = Class.create();
UserDirectoryConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'everyone',
		label: 'Public',
		roles: ''
	},{
		name: 'admin',
		label: 'User Admin',
		roles: 'user_admin,admin'
	}],

	state: [{
		name: 'active',
		label: 'Active'
	},{
		name: 'inactive',
		label: 'Inactive'
	},{
		name: 'all',
		label: 'All'
	}],

	table: {
		everyone: [{
			name: 'sys_user',
			displayName: 'User',
			active: {
				filter: 'active=true',
				fields: 'name,title,department,location,manager',
				btnarray: [],
				refmap: {},
				actarray: []
			},
			inactive: {
				filter: 'active=true^active=false',
				fields: 'name,title,department,location,manager',
				btnarray: [],
				refmap: {},
				actarray: []
			},
			all: {
				filter: 'active=true',
				fields: 'name,title,department,location,manager',
				btnarray: [],
				refmap: {},
				actarray: []
			}
		}],
		admin: [{
			name: 'sys_user',
			displayName: 'User',
			active: {
				filter: 'active=true',
				fields: 'user_name,name,title,department,location',
				btnarray: [{
					name: 'edit',
					label: 'Edit',
					heading: 'Edit',
					icon: 'edit',
					color: '',
					hint: '',
					page_id: 'form'
				}],
				refmap: {},
				actarray: []
			},
			inactive: {
				filter: 'active=false',
				fields: 'user_name,name,title,department,location',
				btnarray: [{
					name: 'edit',
					label: 'Edit',
					heading: 'Edit',
					icon: 'edit',
					color: '',
					hint: '',
					page_id: 'form'
				}],
				refmap: {},
				actarray: []
			},
			all: {
				filter: 'true',
				fields: 'user_name,name,title,department,location,active',
				btnarray: [{
					name: 'edit',
					label: 'Edit',
					heading: 'Edit',
					icon: 'edit',
					color: '',
					hint: '',
					page_id: 'form'
				}],
				refmap: {},
				actarray: []
			}
		}]
	},

	type: 'UserDirectoryConfig'
});

Now we can go back into the Page Designer and edit the content selector widget options to specify our new configuration script.

Editing the content selector widget options in the Page Designer

Now all that is left to do is to go out to the Service Portal and give it a go. Let’s see how it comes out.

First look at the new User Directory portal page

I like it! I clicked around and tried a few things. Overall, I think it came out pretty good, but I did come across one flaw already: clicking on someone’s department or location will take you to the user_profile page and get you a nasty error message because there is no user on file with the sys_id of a department or location. That’s not good. That also brings up an interesting point: I don’t really want to send regular users to the form page for departments or locations, either. I need to find a more appropriate destination for those links. I guess I won’t be wrapping this all up in a bow today after all. I’ll have to give this one some thought, so this looks like an issue that we will have to take up next time out.

Adding Detail to the Upgrade History Task

“Ask not that the journey be easy; ask instead that it be worth it.”
John F. Kennedy

With the addition of the Paris Upgrade Center, a new type of Task was introduced, the Upgrade History Task. Whenever you upgrade your instance, issues that require your attention will now produce an Upgrade History Task, which you can assign to the appropriate resources for resolution. This is a nice feature, and fairly well implemented, but the form layout is not quite organized in the way that I would like to see it, and there are some tidbits of data from various other, related tables that I would really like to see all in one place. Another thing that annoys me is that the Upgrade Details Related Link opens up a new window and breaks out of the iFrame, losing the header and sidebar menu. That page has a link back to the Upgrade History Task, and if you click back and forth a few times, suddenly you have all kinds of windows open, and none of them have navigation anymore. I don’t like that.

So, I thought about making a bunch of UI Formatters and rearranging the stock form layout to include all of the information that I like to see when I am working an upgrade issue, but the more upgrades that I work on, the less I like to tinker with stock components. Ultimately, I decided to just add a single UI Action that would pop up a modal dialog box that contained the information that I was looking for. Here are the things that I wanted to see:

  • A standard link to the Skip record that didn’t open up a new window,
  • The name of the affected table,
  • A link to the affected record,
  • When the affected record was last updated,
  • Who last updated the affected record,
  • Any indication that we have dealt with this issue before, and if so,
  • The details of what was done with this the last time that it came up in an upgrade.

None of that is present on the form right now, but I didn’t see any reason that we couldn’t pull it all together from the various sources, so I went to work. It seemed like there would be a bit of database querying to get all of this information, and I didn’t really want all of that on the page itself, so I started out by making myself a little Script Include to house all of major code. I called it UpgradeTaskUtils and created a single function called getAdditionalInfo to pull together all of the data.

var UpgradeTaskUtils = Class.create();
UpgradeTaskUtils.prototype = {
    initialize: function() {
    },

    getAdditionalInfo: function(sysId) {
		var details = {};

		// all of the hard work goes here!

		return details;
	},

	type: 'UpgradeTaskUtils'
};

The first thing that we will need to do, of course, is to use the incoming sysId argument to go out and get the Upgrade History Record, and once we do that, we can then pull out the associated Skip record.

details.taskGR = new GlideRecord('upgrade_history_task');
if (details.taskGR.get(sysId)) {
	details.logGR = details.taskGR.upgrade_detail.getRefRecord();
	if (details.logGR && details.logGR.isValidRecord()) {
		// more code to follow ...
	}
}

Once we know that we have a valid Skip record, the next thing that we will want to do is go get the actual record that has the issue. That’s a little more complicated and uses a table called sys_metadata.

var metaGR = new GlideRecord('sys_metadata');
if (metaGR.get('sys_update_name', details.logGR.file_name.toString())) {
	details.recordGR = new GlideRecord(metaGR.sys_class_name);
	if (details.recordGR.get(metaGR.sys_id)) {
		details.lastRecordUpdate = details.recordGR.getDisplayValue('sys_updated_on');
		details.lastRecordUpdateBy = details.recordGR.getDisplayValue('sys_updated_by');
		// more code to follow ...
	}
}

Since the sys_updated_by fields is just a user_name string and not an actual reference to a User record, if we want to have the details on the User who last updated the record, we will need to go out and fetch that separately.

var userGR = new GlideRecord('sys_user');
if (userGR.get('user_name', details.lastRecordUpdateBy)) {
	details.userName = userGR.getDisplayValue('name');
	details.userSysId = userGR.getUniqueValue();
	details.userLink = '<a href="/sys_user.do?sys_id=' + details.userSysId + '">' + details.userName + '</a>';
}

That takes care of all of the interrelated records involved with this Task, but there is still more work to do if we want any historical data for this same artifact in previous upgrades. Basically, we want to find all of the records in the Upgrade History Task table that reference this same component, except for the one that we already have. We can just do a quick count to start off with, just to see if there is any point in looking any further.

details.previousIssues = 0;
details.prevIssueQuery = 'upgrade_detail.file_name=' + details.taskGR.upgrade_detail.file_name + '^number!=' + details.taskGR.number;
var taskGA = new GlideAggregate('upgrade_history_task');
taskGA.addAggregate('COUNT');
taskGA.addEncodedQuery(details.prevIssueQuery);
taskGA.query();
if (taskGA.next()) {
	details.previousIssues = taskGA.getAggregate('COUNT');
}

Now that we have a count of what’s out there, we can gather up all of the details if the count is greater than zero.

details.prevIssueLink = details.previousIssues;
if (details.previousIssues > 0) {
	details.prevIssueLink = '<a href="/upgrade_history_task_list.do?sysparm_query=' + details.prevIssueQuery + '">' + details.previousIssues + '</a>';
	var taskGR = new GlideRecord('upgrade_history_task');
	taskGR.addEncodedQuery(details.prevIssueQuery);
	taskGR.orderByDesc('sys_created_on');
	taskGR.query();
	if (taskGR.next()) {
		details.previousUpgrade = taskGR.getDisplayValue('upgrade_detail.upgrade_history.to_version');
		details.previousComments = taskGR.getDisplayValue('upgrade_detail.comments');
	}
}

That’s everything that I am looking for right at the moment. I may end up going back one day and tossing in a few more items, but for now, this should do the trick. All together, the new Script Include looks like this:

var UpgradeTaskUtils = Class.create();
UpgradeTaskUtils.prototype = {
    initialize: function() {
    },

    getAdditionalInfo: function(sysId) {
		var details = {};

		details.taskGR = new GlideRecord('upgrade_history_task');
		if (details.taskGR.get(sysId)) {
			details.logGR = details.taskGR.upgrade_detail.getRefRecord();
			if (details.logGR && details.logGR.isValidRecord()) {
				var metaGR = new GlideRecord('sys_metadata');
				if (metaGR.get('sys_update_name', details.logGR.file_name.toString())) {
					details.recordGR = new GlideRecord(metaGR.sys_class_name);
					if (details.recordGR.get(metaGR.sys_id)) {
						details.lastRecordUpdate = details.recordGR.getDisplayValue('sys_updated_on');
						details.lastRecordUpdateBy = details.recordGR.getDisplayValue('sys_updated_by');
						var userGR = new GlideRecord('sys_user');
						if (userGR.get('user_name', details.lastRecordUpdateBy)) {
							details.userName = userGR.getDisplayValue('name');
							details.userSysId = userGR.getUniqueValue();
							details.userLink = '<a href="/sys_user.do?sys_id=' + details.userSysId + '">' + details.userName + '</a>';
						}
					}
				}
			}
			details.previousIssues = 0;
			details.prevIssueQuery = 'upgrade_detail.file_name=' + details.taskGR.upgrade_detail.file_name + '^number!=' + details.taskGR.number;
			var taskGA = new GlideAggregate('upgrade_history_task');
			taskGA.addAggregate('COUNT');
			taskGA.addEncodedQuery(details.prevIssueQuery);
			taskGA.query();
			if (taskGA.next()) {
				details.previousIssues = taskGA.getAggregate('COUNT');
			}
			details.prevIssueLink = details.previousIssues;
			if (details.previousIssues > 0) {
				details.prevIssueLink = '<a href="/upgrade_history_task_list.do?sysparm_query=' + details.prevIssueQuery + '">' + details.previousIssues + '</a>';
				var taskGR = new GlideRecord('upgrade_history_task');
				taskGR.addEncodedQuery(details.prevIssueQuery);
				taskGR.orderByDesc('sys_created_on');
				taskGR.query();
				if (taskGR.next()) {
					details.previousUpgrade = taskGR.getDisplayValue('upgrade_detail.upgrade_history.to_version');
					details.previousComments = taskGR.getDisplayValue('upgrade_detail.comments');
				}
			}
		}

		return details;
	},

	type: 'UpgradeTaskUtils'
};

Now we need put all of this data on page that we can use for our modal pop-up. I created a new UI Page called upgrade_history_task_info, and started it out by calling our new Script Include to obtain the data.

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
	<g:evaluate var="jvar_not_used">
var utu = new UpgradeTaskUtils();
var obj = utu.getAdditionalInfo(RP.getWindowProperties().get('task_sys_id'));
obj.lastRecordUpdate;		
	</g:evaluate>
	<div style="padding: 20px;">
		<!-- display grid goes here -->
	</div>
</j:jelly>

To format the data, I just used a basic HTML table, with one column for the labels and another for the data.

<table>
	<tr>
		<th style="padding: 5px;">Issue Details:</th>
		<td style="padding: 5px;"><a href="/${obj.logGR.getTableName()}.do?sys_id=${obj.logGR.getUniqueValue()}">${obj.logGR.getDisplayValue()}</a></td>
	</tr>
	<tr>
		<th style="padding: 5px;">Affected Table:</th>
		<td style="padding: 5px;">${obj.recordGR.getLabel()} (${obj.recordGR.getTableName()})</td>
	</tr>
	<tr>
		<th style="padding: 5px;">Affected Record:</th>
		<td style="padding: 5px;"><a href="/${obj.recordGR.getTableName()}.do?sys_id=${obj.recordGR.getUniqueValue()}">${(obj.recordGR.getDisplayValue() > ''?obj.recordGR.getDisplayValue():obj.recordGR.getUniqueValue())}</a></td>
	</tr>
	<tr>
		<th style="padding: 5px;">Record Last Updated:</th>
		<td style="padding: 5px;">${obj.recordGR.getDisplayValue('sys_updated_on')}</td>
	</tr>
	<tr>
		<th style="padding: 5px;">Record Last Updated By:</th>
		<td style="padding: 5px;"><g:no_escape>${(obj.userSysId > ''?obj.userLink:obj.recordGR.getDisplayValue('sys_updated_by'))}</g:no_escape></td>
	</tr>
	<tr>
		<th style="padding: 5px;">Previous Upgrade Issues:</th>
		<td style="padding: 5px;"><g:no_escape>${obj.prevIssueLink}</g:no_escape></td>
	</tr>
	<j:if test="${obj.previousIssues > 0}">
		<tr>
			<th style="padding: 5px;">Last Upgrade w/Issue:</th>
			<td style="padding: 5px;">${obj.previousUpgrade}</td>
		</tr>
		<tr>
			<th style="padding: 5px;">Last Upgrade Comments:</th>
			<td style="padding: 5px;">${obj.previousComments}</td>
		</tr>
	</j:if>
</table>

Now we have our data and we have it laid out nicely on a page, all that’s left to do is to pop it up on the screen. For that, we will need to build a UI Action. I called mine Additional Info, linked it to the Upgrade History Task table, and gave it the following onClick script:

function openAdditionalInfo() {
	var dialog = new GlideDialogWindow('upgrade_history_task_info');
	dialog.setSize(600, 600);
	dialog.setTitle('Additional Info');
	dialog.setPreference('task_sys_id', NOW.sysId);
	dialog.render();
}

That’s pretty much all there is to that. We still need to pull it up and click on it and see what happens, but assuming that all goes well, this exercise should have produced a nice little tool to make plodding through the skipped records in an upgrade just a little bit easier.

Additional Info modal pop-up on the Upgrade History Task form

Nice! Now all we need to do is gather up all of the parts and stuff them into an Update Set.

Bulk Actions in the Service Portal Data Table Widget

“Innovation comes from people who take joy in their work.”
W. Edwards Deming

Since the day that I first touched the Data Table widget, I have somehow kept finding reasons to go back and tinker with it some more for one reason or another. While I was playing around with the buttons and icons feature that I added, I noticed that one of the other features that was present in the primary UI, but missing in the Service Portal Data Table widget, was the ability to select more than one row and perform some action on all of the selected rows. That got the little wheels spinning around inside of my head and I started trying to figure out just what it would take to implement that feature in my hacked up version of the stock widget. It seemed to me that you would need a number of things:

  • A way to pass in one or more bulk actions as part of the configuration,
  • A master checkbox in the headings that you could use to toggle all of the individual checkboxes off and on,
  • A checkbox on every row,
  • A select statement in the footer with the choices being all of the specified bulk actions, and
  • Some client side scripts to handle the clicks on the master checkbox and the action selector.

We already pass in an Array of Buttons/Icons, so why not an Array of Bulk Actions? We should be able to copy most of that code wherever it lives, since this would pretty much be handled in the same way. Having at least one item in the Array could drive the visibility of the checkboxes and select statement, and as far as processing the Action is concerned, we could take the same approach that we took with the buttons and just broadcast the selected action and let some other widget deal with the actual response whenever a Bulk Action was clicked. It all seemed simple enough to give it a go, so I started hacking up the HTML, first for the master checkbox in the heading row:

<th ng-if="data.actarray.length > 0" class="text-nowrap center" tabindex="0">
  <input type="checkbox" ng-model="data.master_checkbox" ng-click="masterCheckBoxClick();"/>
</th>

Then I did the same for the individual checkboxes in the data rows:

<td ng-if="data.actarray.length > 0" class="text-nowrap center" tabindex="0">
  <input type="checkbox" ng-model="item.selected"/>
</td>

And then to finish out the HTML changes, I added an extra footer down at the bottom for the SELECT element:

<div class="panel-footer" ng-if="data.actarray.length > 0 && data.row_count">
  <div class="btn-toolbar m-r pull-left">
    <select class="form-control" ng-model="data.bulk_action" ng-click="bulkActionSelected();">
      <option value="">${Actions on selected rows ...}</option>
      <option ng-repeat="opt in data.actarray" value="{{opt.name}}">{{opt.label}}</option>
    </select>
  </div>
  <span class="clearfix"></span>
</div>

That took care of the HTML. On the server-side code, I added a couple of new items to the comments explaining all of the various options, and then added the bulk actions to the list of things that get copied in:

 * data.buttons = the JSON string containing the button specifications
 * data.btnarray = the array of button specifications
 * data.refpage = the JSON string containing the reference link specifications
 * data.refmap = the reference link specifications object
 * data.bulkactions = the JSON string containing the bulk action specifications
 * data.actarray = the bulk actions specifications object
 */
// copy to data[name] from input[name] || option[name]
optCopy(['table', 'buttons', 'btns', 'refpage', 'bulkactions', 'p', 'o', 'd', 'filter',
	'filterACLs', 'fields', 'field_list', 'keywords', 'view', 'relationship_id',
	'apply_to', 'apply_to_sys_id', 'window_size', 'show_breadcrumbs']);

I also copied the code that converts the buttons string into the btnarray array and hacked it up to convert the bulkactions string into an actarray array,

if (data.bulkactions) {
	try {
		var actioninfo = JSON.parse(data.bulkactions);
		if (Array.isArray(actioninfo)) {
			data.actarray = actioninfo;
		} else if (typeof actioninfo == 'object') {
			data.actarray = [];
			data.actarray[0] = actioninfo;
		} else {
			gs.error('Invalid bulk actions in SNH Data Table widget: ' + data.bulkactions);
			data.actarray = [];
		}
	} catch (e) {
		gs.error('Unparsable bulk actions in SNH Data Table widget: ' + data.bulkactions);
		data.actarray = [];
	}
} else {
	data.actarray = [];
}

Over on the client side, I added two functions to the $scope, one for the master check box:

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

… and one for the bulk action selection:

$scope.bulkActionSelected = function() {
	if (c.data.bulk_action) {
		var parms = {};
		parms.table = c.data.table;
		parms.selected = [];
		for (var x in c.data.list) {
			if (c.data.list[x].selected) {
				parms.selected.push(c.data.list[x]);
			}
		}
		if (parms.selected.length > 0) {
			for (var b in c.data.actarray) {
				if (c.data.actarray[b].name == c.data.bulk_action) {
					parms.action = c.data.actarray[b];
				}
			}
			$rootScope.$emit(eventNames.bulkAction, parms);
		} else {
			spModal.alert('You must select at least one row for this action');
		}
	}
	c.data.bulk_action = '';
};

That took care of the root Data Table widget, but I still needed to do a little work on the SNH Data Table from URL Definition widget to pull our new query parameter down from the URL. That turned out to be just a simple addition to this line to add the new parameter name to the list of parameters to be copied:

copyParameters(data, ['p', 'o', 'd', 'filter', 'buttons', 'refpage', 'bulkactions']);

I also needed to copy the button code in the Content Selector widget to create similar code for the new bulk actions:

if (tableInfo[state].actarray && Array.isArray(tableInfo[state].actarray) && tableInfo[state].actarray.length > 0) {
	s.bulkactions = JSON.stringify(tableInfo[state].actarray);
}

Now we can go into the ButtonTestConfig Script Include that we built the other day and add a couple of bulk actions so that we can test this out:

Adding a few bulk actions to the test configuration

Now, let’s pull up our button_test page and see what we’ve got.

First look at our bulk actions modifications

Not too bad … we have our master checkbox in the heading row, the individual check boxes in the data rows, and our bulk action selector in the new extra footer. Very nice. And we can even test for the requirement that you have to select at least one item by selecting an action without selecting any rows.

Selecting an action without selecting any rows

Well, that seems to work. The master checkbox also seemed to work as desired, and selecting a few rows and then selecting an action also seemed to work, but since there is currently no one listening on the other end, it’s kind of hard to tell if that actually did anything or not. Maybe we can modify our Button Click Handler Example widget to listen for bulk actions as well. Maybe add something like this:

$rootScope.$on('data_table.bulkAction', function(e, parms) {
	displayBulkActionDetails(parms);
});

function displayBulkActionDetails(parms) {
	var html = '<div>'; 
	html += ' <div class="center"><h3>You selected the ' + parms.action.name + ' bulk action</h3></div>\n';
	html += ' <table>\n';
	html += '  <tbody>\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">Action: &nbsp;</td>\n';
	html += '    <td><pre>' + JSON.stringify(parms.action, null, 4) + '</pre></td>\n';
	html += '   </tr>\n';
	html += '   <tr>\n';
	html += '    <td class="text-primary">Records: &nbsp;</td>\n';
	html += '    <td><pre>' + JSON.stringify(parms.selected, null, 4) + '</pre></td>\n';
	html += '   </tr>\n';
	html += '  </tbody>\n';
	html += ' </table>\n';
	html += '</div>';
	spModal.alert(html);
}

Let’s give that a whirl …

Bulk action listener results

Beautiful. It all appears to work as intended. Clearly some additional testing is warranted, but it’s not bad for an initial effort. I think it’s good enough to release an Update Set with all of the code. Of course, now we have broken our new Content Selector Configuration Editor, since that was not built to handle bulk actions, but that’s a problem for another day.

Flow Designer Counter

“If you have built castles in the air, your work need not be lost; that is where they should be. Now put the foundations under them.”
Henry David Thoreau

Since I first threw together my Flow Designer Array Iterator, I have had a number of occasions to put it to good use, but recently I had a need for an iterating index, but I did not have a String Array to use as the basis for the iteration. I thought about just creating one, just to have something to pass in to the Action, but then it occurred to me that it might be useful to have some kind of simple index action that wasn’t dependent on the presence of a String Array. Basically, it could be very similar to my Array Iterator, but without the array.

I thought about making a new Script Include just for these new Actions, but I decided to just add them to my existing SNHStringArrayUtils Script Include, mainly because I wanted to copy the existing functions from there, anyway, and so it was easier to just drop the copy right there into the same script. To start with, I copied the createIterator function and then hacked it up to create a new createCounter function.

createCounter: function(scratchpadId, counterId) {
	var response = {};

	var snhspu = new SNHScratchpadUtils();
	response = snhspu.setScratchpadProperty(scratchpadId, counterId, 0);
	if (response.success) {
		response.message = 'Counter ' + counterId + ' successfully created';
	}

	return response;
}

For the most part, that was just a matter of removing all of the code related to the String Array, which we are not using here, and then setting the value of our new counter to zero. Once that was done, I copied the existing iteratorNext function and hacked that up to create the counterNext function.

counterNext: function(scratchpadId, counterId) {
	var response = {};

	var snhspu = new SNHScratchpadUtils();
	response = snhspu.getScratchpadProperty(scratchpadId, counterId);
	if (response.success) {
		var counter = parseInt(response.property_value);
		var previous = counter;
		counter++;
		var current = counter;
		counter++;
		var next = counter;
		response = snhspu.setScratchpadProperty(scratchpadId, counterId, current);
		if (response.success) {
			response.previous_value = previous;
			response.current_value = current;
			response.next_value = next;
			response.message = 'The current value of the counter is ' + current;
		}
	}

	return response;
}

That was pretty much it for the scripting changes. With that all put to bed, I popped over to the Flow Designer and basically did the same thing with my array Actions that I did with the array functions, copy them and then modify them for my new purpose. I used my Create Array Iterator Action as the starting point for my new Create Counter Action, and then used my Array Iterator Next Action as the basis of my new Increment Counter action. Once again, I spent more time removing things than I did adding things, and it all went relatively quickly. The only thing to do now was to do test it all out.

As with the array Actions, you have to first have a Scratchpad on which to store the values, so I ran a quick test on my Create Scratchpad Action to get a fresh Scratchpad ID. With that in hand, I pulled up my new Create Counter Action and hit the Test button, entered my Scratchpad ID and Counter ID and ran the test.

Testing the Create Counter Action

That all seemed to work out OK, so I pulled up my new Increment Counter Action and gave that one a whirl as well. In fact, I ran the test a few times, just to run up the number.

Increment Counter Action test results

Well, everything seems to work. Obviously, there is a lot more testing to do in order to check out all of the error handling built into the processes, but the main purpose of the exercise seems to be satisfied, so that’s always a good thing. If you want to play around with all of the various parts and pieces, here’s an Update Set that should contain everything that you would need.

Note: With the introduction of Flow Variables, this component is no longer necessary.

Flow Designer Array Iterator

“It is by logic that we prove, but by intuition that we discover.”
Henri Poincaré

One of the reasons that I built my little Flow Designer Scratchpad was to keep track of an index while I looped through items in an Array. After doing a few of those, I decided it would be even nicer if I had some kind of iterator Action that would do the work of incrementing the index and returning the Array element at the current index. I already had the scratchpad to store all of the data necessary to support an iterator, so it seemed as if I could write some kind of Action that would take a Scratchpad ID, an Iterator ID, and an Array as input and use that information to set up the ability to iterate through the Array provided. As usual, I decided to put the bulk of the code in a Script Include to keep the actual code in the Action down to the absolute minimum. Here is the SNHStringArrayUtils that I came up with:

var SNHStringArrayUtils = Class.create();
SNHStringArrayUtils.prototype = {
	initialize: function() {
	},

	createIterator: function(scratchpadId, interatorId, stringArray) {
		var response = {};

		var snhspu = new SNHScratchpadUtils();
		response = snhspu.setScratchpadProperty(scratchpadId, interatorId, interatorId);
		if (response.success) {
			if (Array.isArray(stringArray)) {
				var iterator = {};
				iterator.index = 0;
				iterator.array = stringArray;
				response = snhspu.setScratchpadProperty(scratchpadId, interatorId, JSON.stringify(iterator));
				if (response.success) {
					response.message = 'Array Interator ' + interatorId + ' successfully created';
				}
			} else {
				response.success = false;
				response.message = 'String Array parameter is missing or invalid';
			}
		}

		return response;
	},

	iteratorNext: function(scratchpadId, interatorId) {
		var response = {};

		var snhspu = new SNHScratchpadUtils();
		response = snhspu.getScratchpadProperty(scratchpadId, interatorId);
		if (response.success) {
			var iterator = {};
			try {
				iterator = JSON.parse(response.property_value);
			} catch (e) {
				response.success = false;
				response.message = 'Unable to parse JSON string containing iterator details';
			}
			if (response.success) {
				if (iterator.index >= 0 && iterator.index < iterator.array.length) {
					response.current_value = iterator.array[iterator.index];
					response.current_index = iterator.index;
					iterator.index++;
					response.has_next = (iterator.index < iterator.array.length);
					response.message = 'The current value at index ' + response.current_index + ' is ' + response.current_value;
					snhspu.setScratchpadProperty(scratchpadId, interatorId, JSON.stringify(iterator));
				} else {
					response.success = false;
					response.message = 'Current index value out of range';
				}
			}
		}

		return response;
	},

	type: 'SNHStringArrayUtils'
};

Basically, there are two methods, one for each of the two Flow Designer Actions that I intend to build. The first one is createIterator, which is used to initialize a new iterator, and the second is iteratorNext, which will support the Action that you will invoke inside of your loop to get the next item in the Array. Both utilize an existing scratchpad, so you will need to create that prior to invoking these Actions, and both require an Iterator ID, which is just a unique key to be used in storing the iterator data in the scratchpad. The createIterator action would be called once outside of the loop, and then the iteratorNext function would be called inside of the loop, usually right at the top to pull out the next value in the array.

The iterator itself is just a two-property object containing the array and the current value of the index. This is converted to a JSON string and stuffed into the scratchpad using the passed Iterator ID as the key. When creating the iterator, we set the index value to zero, and in the next Action, after using the index to get the current element, we increment it and update the scratchpad. Now that we have the basic code to support the two Actions, we need to go into the Flow Designer and create the Actions.

The Create Array Iterator Action seems like the logical place to start. That will will need three Inputs defined.

The Create Array Iterator Action Inputs

… and it will need two Outputs defined, a success indicator and an optional error message detailing any failure to perform its intended function.

The Create Array Iterator Action Outputs

In between the Inputs and Outputs will be a simple Script step, where we will produce the Outputs by passing the Inputs to our associated Script Include function.

var snhsau = new SNHStringArrayUtils();
var result = snhsau.createIterator(inputs.scratchpad_id, inputs.iterator_id, inputs.string_array);
for (var key in result) {
	outputs[key] = result[key];
}

That’s pretty much all there is to that. We can test it using the Test button up at the top of the Action Editor, but first we will need a Scratchpad. We can take care of that real quick by hitting the Test button on the Create Scratchpad Action and then grabbing the Scratchpad ID from the Outputs. With our Scratchpad ID in hand, we can now test our Create Array Iterator Action.

Testing the Create Array Iterator Action

So far so good. now we just need to do the same thing for the Array Iterator Next Action, and we’ll be all set.

The Array Iterator Next Action

This is pretty much a rinse and repeat kind of thing, with fewer Inputs, but more Outputs. When it comes time to test, we can use the Scratchpad ID and Iterator ID from our last test, and then run it through a few times to see the different results at different stages of the process. Rather that go through all of that here, I will just bundle everything up into an Update Set, and you can pull it down and play with it on your own.

Note: With the introduction of Flow Variables, this component is no longer necessary.

Flow Designer Scratchpad

“Necessity is the mother of invention.”
English-language proverb

I really should be working on testing out the latest enhancements to my Dynamic Service Portal Breadcrumbs right now, but I ran into this other issue recently, and I really want to see if I can make this work. I hate it when people start something and then move on to other things without ever finishing up what they started, so I definitely want to circle back and wrap that one up; however, today is not that day.

Today I want to talk about the Flow Designer. I have been striving to convert any of my old legacy Workflows over to the newer Flow Designer tool whenever the opportunity arises. The other day I was doing just that with a Workflow that made extensive use of the Workflow Scratchpad feature. When I went to look for the equivalent feature in the Flow Designer, I couldn’t find anything. I thought maybe that it wasn’t needed for some reason, so I tried several workarounds, but nothing worked. Nothing that I tried would preserve and/or modify data between or across Actions or Subflows. After quite a number of failed attempts to find something that would do the job, I eventually came to realize that if I wanted some kind of Scratchpad capability in the Flow Designer, I was going to have to build it myself.

My first thought was that all that I would need would be simple setProperty and getProperty functions, but then I realized that I would first need to establish the scratchpad, and once established, I would want to be able to get rid of it as well, so that turned into four relatively simple functions, which is still not too bad. When I say functions, what I really mean are Flow Designer Actions, but since I will be calling some function in a common Script Include built for this purpose, I still think of them as functions. Here is the function to create a scratchpad, which is just basically a record on a table that I created for this purpose:

createScratchpad: function() {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	spGR.initialize();
	spGR.setValue('u_scratchpad', '{}');
	if (spGR.insert()) {
		response.success = true;
		response.scratchpad_id = spGR.getUniqueValue();
		response.message = 'Scratchpad record successfully created';
	} else {
		response.success = false;
		response.message = 'Unable to create scratchpad record';
	}

	return response;
},

The scratchpad itself is just a JSON string stored in the only column added to the table, u_scratchpad. We initialize that to an empty object and save the record and that’s about all there is to that. To get rid of it, we will need to have the sys_id of the record, but there is not much code behind that process, either:

deleteScratchpad: function(spId) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		if (spGR.deleteRecord()) {
			response.success = true;
			response.message = 'Scratchpad record successfully deleted';
		} else {
			response.success = false;
			response.message = 'Unable to delete scratchpad record';
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

That takes care of building up and tearing down the scratchpad object. Now, to use it, we will need those setProperty and getProperty functions that we were talking about earlier. This one will let you set the value of a property on the scratchpad:

setScratchpadProperty: function(spId, propertyName, propertyValue) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		var jsonString = spGR.getValue('u_scratchpad');
		var jsonObject = {};
		try {
			jsonObject = JSON.parse(jsonString);
		} catch(e) {
			response.warning = 'Unable to parse JSON string: ' + e;
		}
		jsonObject[propertyName] = propertyValue;
		jsonString = JSON.stringify(jsonObject, null, '\t');
		spGR.setValue('u_scratchpad', jsonString);
		if (spGR.update()) {
			response.success = true;
			response.message = 'Scratchpad property "' + propertyName + '" set to "' + propertyValue + '"';
		} else {
			response.success = false;
			response.message = 'Unable to update scratchpad record';
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

… and this one lets you retrieve the value of a property on the scratchpad:

getScratchpadProperty: function(spId, propertyName) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		var jsonString = spGR.getValue('u_scratchpad');
		try {
			var jsonObject = JSON.parse(jsonString);
			var propertyValue = jsonObject[propertyName];
			if (propertyValue > '') {
				response.success = true;
				response.property_value = propertyValue;
				response.message = 'Returning value "' + propertyValue + '" for scratchpad property "' + propertyName + '"';
			} else {
				response.success = false;
				response.message = 'Scratchpad property "' + propertyName + '" has no value';
			}
		} catch(e) {
			response.success = false;
			response.message = 'Unable to parse JSON string: ' + e;
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

That’s it for the core functions needed to make this work. Putting it all together, the entire Script Include looks like this:

var SNHScratchpadUtils = Class.create();
SNHScratchpadUtils.prototype = {
    initialize: function() {
    },

	createScratchpad: function() {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		spGR.initialize();
		spGR.setValue('u_scratchpad', '{}');
		if (spGR.insert()) {
			response.success = true;
			response.scratchpad_id = spGR.getUniqueValue();
			response.message = 'Scratchpad record successfully created';
		} else {
			response.success = false;
			response.message = 'Unable to create scratchpad record';
		}

		return response;
	},

	setScratchpadProperty: function(spId, propertyName, propertyValue) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			var jsonString = spGR.getValue('u_scratchpad');
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch(e) {
				response.warning = 'Unable to parse JSON string: ' + e;
			}
			jsonObject[propertyName] = propertyValue;
			jsonString = JSON.stringify(jsonObject, null, '\t');
			spGR.setValue('u_scratchpad', jsonString);
			if (spGR.update()) {
				response.success = true;
				response.message = 'Scratchpad property "' + propertyName + '" set to "' + propertyValue + '"';
			} else {
				response.success = false;
				response.message = 'Unable to update scratchpad record';
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	getScratchpadProperty: function(spId, propertyName) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			var jsonString = spGR.getValue('u_scratchpad');
			try {
				var jsonObject = JSON.parse(jsonString);
				var propertyValue = jsonObject[propertyName];
				if (propertyValue > '') {
					response.success = true;
					response.property_value = propertyValue;
					response.message = 'Returning value "' + propertyValue + '" for scratchpad property "' + propertyName + '"';
				} else {
					response.success = false;
					response.message = 'Scratchpad property "' + propertyName + '" has no value';
				}
			} catch(e) {
				response.success = false;
				response.message = 'Unable to parse JSON string: ' + e;
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	deleteScratchpad: function(spId) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			if (spGR.deleteRecord()) {
				response.success = true;
				response.message = 'Scratchpad record successfully deleted';
			} else {
				response.success = false;
				response.message = 'Unable to delete scratchpad record';
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	type: 'SNHScratchpadUtils'
};

Now that we have all of the functions, we need to turn those into Flow Designer Actions. Before we do that, though, let’s create a Category for them so that we can group them and they will be easy to find. We do that by adding a row to the Action Category table, sys_hub_category. With that out of the way, we can create our first Action, which will invoke the createScratchpad function in our Script Include.

Create Scratchpad Action

The entire Action is just a Script step that leverages our Script Include and passes the results on to the Action Outputs. The small script to make that happen is just a few short lines of code:

var snhspu = new SNHScratchpadUtils();
var result = snhspu.createScratchpad();
for (var key in result) {
	outputs[key] = result[key];
}

Now we just need to repeat that process 3 more times to create Flow Designer Actions from our three other Script Include functions and we’re all set. To test things out, there is a little Test button right at the top of the Flow Designer page, and for the Create Scratchpad Action, there isn’t even any input to set up, so you can just click that button and go. Once you test out the Create Scratchpad Action, you can snag the Scratchpad ID out of the Action Outputs and then use that as an input to test all of the others.

Well, that wasn’t so bad: one Script Include, four functions, one Action Category, and four Actions. I threw this together rather quickly, but here is the Update Set. If you run into any issues with that, or if you can think of any way to make it better, please let me know in the comments. Of if you know of a built-in function that eliminates the need for this, that would be even better!

Note: With the introduction of Flow Variables, this component is no longer necessary.

Using the GlideExcelParser on a Fetched Remote File

“Do not go where the path may lead, go instead where there is no path and leave a trail.”
Ralph Waldo Emerson

When we hacked the REST Message API to fetch a remote file, all we did with the file was to attach it to an existing Incident. But there are times when what you are really after is the contents of the file, not the file itself. When that file happens to be an Excel spreadsheet, the ServiceNow platform comes shipped with a handy tool called the GlideExcelParser that will allow you to dig into that file and pull out the data. By combining this capability with our earlier work on fetching remote files, you can reach out to a URL, pull in a spreadsheet from another site, and then parse the spreadsheet to extract the data that you need.

One example of where this might be useful would be in importing data records where one or more fields on the record is a URL of an associated spreadsheet containing relevant information such a list of items to be ordered or a list of people for which you would like to place an order. Another example might be a mail-in ticket where the details are not included in the body of the email or attached to the email as a file, but accessible via a URL link found in the message body. Basically, this applies to any scenario where you have the URL of a file, and just want the data contained in the file and not the actual file.

To demo this capability, we just need to find a spreadsheet hosted on the Internet that we can use for an example. Something like this:

https://www.servicenow.com/content/dam/servicenow-assets/public/en-us/doc-type/other-document/csc/servicenow-platform-support-calculator-template.xlsx

This document has three sheets, but for our purposes we can just focus on one of the areas of one of the sheets, just to show how things work. Here is the second sheet, labeled Input:

Example spreadsheet to use for demonstration purposes

For our little demo, we will focus on the grey block on rows 20 through 31 so that we can pull out some data and then use it for some other purpose such as creating an Incident. Once we pull out the data, we will no longer need the actual file, so we can delete the attachment that we created to tidy things up when we are done. Since this is just a demo, we can do the whole thing in a Fix Script that we can discard later once our little proof of concept has been completed.

The first thing that we will need to do is to go get our file. For that, we can leverage our earlier Script Include, but we will need to attach it to something while we work with it. For the purposes of this demonstration, let’s just attach it to the Fix Script itself.

var url = 'https://snhackery.com/update_sets/servicenow-platform-support-calculator-template.xlsx';
var table = 'sys_script_fix';
var sys_id = '0bbdf83e2f21d0104425fcecf699b660';
var fileFetcher = new FileFetchUtils();
var attachment = fileFetcher.fetchFileFromUrlAndAttach(table, sys_id, url);

That was simple enough. Now let’s parse the file.

var stream = new GlideSysAttachment().getContentStream(attachment);
var parser = new sn_impex.GlideExcelParser();
parser.setSheetName('Input');
var row = [];
if (parser.parse(stream)) {
	while (parser.next()) {
		row.push(parser.getRow());
	}
} else {
	gs.info("Error: Unable to parse uploaded file");
}

OK, this one is a little more complicated. First, we create a content stream from the file attachment. Then we create a new parser. Then we tell the parser that we want it to parse the second sheet, the one labeled Input. Then we create an empty array to store the rows, and loop through the parsed stream adding rows to our array one at a time until we run out of rows. Now we have an array of row that that we can use for whatever purpose that we want.

At this point, we can delete the attachment, since we have no further use for it, but let’s save that for a little later and first see what we can do with our row data. To start with, let’s just make sure that we have some data, and if we do, let’s call a function in which we can encapsulate all of the code to utilize the data.

if (row.length) {
	gs.info('Incident created: ' + createIncident(row));
} else {
	gs.info("Error: No data rows in parsed uploaded file");
}

Now it’s time to deal with the data. The parsed rows are values keyed by the column heading. Where there is no column heading, then the key is the Excel column letter. In our little example, we are interested in the data in the second and third columns, and in the first row, there is a value in the second column (Data Input Form), but not in the third. This means that second column values will be obtained with thisRow[‘Data Input Form’] and third column values will be obtained with thisRow[‘C’] (since C is Excel’s name for the third column).

So to rebuild that little grey block of data, we will need the second column value for row #19, the second and third column values for rows 21 through 26, and then the second and third column values for row 28. Now, that’s assuming that no one adds or removes a line or a column from the spreadsheet when filling out the template, which could be dealt with using a lot more defensive coding strategy, but that’s a little beyond the scope of this little exercise here. For our purposes, we are just going to assume that things are going to be right where they are supposed to be. So here is our function.

function createIncident(row) {
	var full_description = 'The following data was extracted from the fetched spreadsheet:\n\n';
	full_description += row[19]['Data Input Form'] + '\n\n';
	for (var i=21; i<27; i++) {
		full_description += '\t' + row[i]['Data Input Form'] + ':\t' + row[i]['C'] + '\n';
	}
	full_description += '\n' + row[28]['Data Input Form'] + '\t' + row[28]['C'];
	var incidentGR = new GlideRecord('incident');
	incidentGR.caller_id = gs.getUserID();
	incidentGR.short_description = 'Excel parsing example';
	incidentGR.description = full_description;
	incidentGR.assignment_group.setDisplayValue('Service Desk');
	incidentGR.insert();
	return incidentGR.getDisplayValue('number');
}

Now that the Incident has been created, we just need to delete the attachment record to clean things up. That’s just a few more lines of code.

var attachmentGR = new GlideRecord('sys_attachment');
attachmentGR.get(attachment);
attachmentGR.deleteRecord();

That’s it. All that is left is to save the script and then click on that little Run Fix Script button to see what happens.

*** Script: Incident created: INC0010019

Let’s go check out our new Incident and see how things came out.

Incident generated from data extracted from Excel spreadsheet

Well, it looks pretty close, but we missed a line of data. The System Administrators line didn’t make it over. Looks like our start index should have been 20 and not 21. Oh well, you get the idea. I would go back and fix it, but this is just a little demo to show how things could work together, and I think we have accomplished that. If you want to play around with this, you will be using your own spreadsheet anyway, so you’ll be doing something completely different. But if you want to fix your own copy of this one, please go right ahead!

Hacking the REST Message API to Fetch a Remote File

“The way to get started is to quit talking and begin doing.”
Walt Disney

The other day I needed to fetch a file that was posted on another web site via its URL and process it. I have processed ServiceNow file attachments before, but I have never attempted to go out to the Internet and pull in a file using an HTTP GET of the URL. There are several ways to do that in Javascript such as XMLHttpRequest or fetch, but none of those seemed to work in server-side code in ServiceNow. But you can open up a URL using the ServiceNow RESTMessageV2 API, so I thought that maybe I would give that a shot. How hard could it be?

I decided to encapsulate everything into a Script Include, mainly so that if I ever needed to do this again, I could call the same function in some other context. My thought was to pass in the URL of the file that I wanted to fetch along with the table name and sys_id of the record to which I wanted the file to be attached, and then have the function fetch the file, attach it, and send back the attachment. Something like this:

var FileFetchUtils = Class.create();
FileFetchUtils.prototype = {
	initialize: function() {
	},

	fetchFileFromUrlAndAttach: function(table, sys_id, url) {
		...
	},

	type: 'FileFetchUtils'
};

That was idea, anyway. Let’s say that I wanted to attach this file, available on the ServiceNow web site, to some Incident:

https://www.servicenow.com/content/dam/servicenow-assets/public/en-us/doc-type/success/playbook/implementation.pdf

The code to do that would look something like this:

var fileFetcher = new FileFetchUtils();
fileFetcher.fetchFileFromUrlAndAttach('incident', incSysId, 'https://www.servicenow.com/content/dam/servicenow-assets/public/en-us/doc-type/success/playbook/implementation.pdf');

All we need to do now is come up with the code needed to do all of the work of fetching the file, attaching it to the specified record, and returning the attachment. To begin, we will need to extract the name of the file from the URL. Assuming that the file name is the last component of the path on the URL, we can do that by splitting the path into its component parts and grabbing the last part.

var parts = url.split('/');
var fileName = parts[parts.length-1];

Next, we will need to create and configure the request object.

var request  = new sn_ws.RESTMessageV2();
request.setHttpMethod('get');
request.setEndpoint(url);

The next thing to do would be to execute the request, but before we do that, we can take advantage of a nice built-in feature that will really simplify this whole operation. There is an available function of the RESTMessageV2 API that allows you to declare your intent to turn the retrieved file into an attachment, which will then handle all of the details of doing that on your behalf when the request is executed. You just need to invoke the function before you execute the request.

request.saveResponseBodyAsAttachment(table, sys_id, fileName);        
response = request.execute();

Although that really makes things super simple, it’s still a good idea to check the HTTP Response Code, just to make sure all went well. If not, it’s a good practice to relay that to the user.

if (response.getStatusCode() == '200') {
	...
} else {
	returnValue = 'Error: Invalid HTTP Response: ' + response.getStatusCode();
}

Now, assuming that things actually did go well and our new attachments was created, we still want to send that back to the calling script as a response to this function. The RESTMessageV2 API saveResponseBodyAsAttachment function does not return the attachment that is created, so we will have to use the table and sys_id to hunt it down. And if we cannot find it for any reason, we will want to report that as well.

var attachmentGR = new GlideRecord('sys_attachment');
attachmentGR.addQuery('table_name', table);
attachmentGR.addQuery('table_sys_id', sys_id);
attachmentGR.orderByDesc('sys_created_on');
attachmentGR.query();
if (attachmentGR.next()) {
	returnValue = attachmentGR.getUniqueValue();
} else {
	returnValue = 'Error: Unable to fetch attachment';
}

That should now be everything that we need to fetch the file, attach it, and send back the attachment. Putting it all together, the entire Script Include looks like this:

var FileFetchUtils = Class.create();
FileFetchUtils.prototype = {
	initialize: function() {
		this.REST_MESSAGE = '19bb0cde2fedd4101a75ad2ef699b6da';
	},

	fetchFileFromUrlAndAttach: function(table, sys_id, url) {
		var returnVlaue = '';
		var parts = url.split('/');
		var fileName = parts[parts.length-1];
		var request  = new sn_ws.RESTMessageV2();
		request.setHttpMethod('get');
		request.setEndpoint(url);
		request.saveResponseBodyAsAttachment(table, sys_id, fileName);        
		response = request.execute();
		if (response.getStatusCode() == '200') {
			var attachmentGR = new GlideRecord('sys_attachment');
			attachmentGR.addQuery('table_name', table);
			attachmentGR.addQuery('table_sys_id', sys_id);
			attachmentGR.orderByDesc('sys_created_on');
			attachmentGR.query();
			if (attachmentGR.next()) {
				returnValue = attachmentGR.getUniqueValue();
			} else {
				returnValue = 'Error: Unable to fetch attachment';
			}
		} else {
			returnValue = 'Error: Invalid HTTP Response: ' + response.getStatusCode();
		}
		return returnValue;
	},

	type: 'FileFetchUtils'
};

Now all we need to do is find an Incident to use for testing and use the background scripts feature to give it a try. First, we’ll need to pull up an Incident and then use the Copy sys_id option of the hamburger drop-down menu to snag the sys_id of the Incident.

Grabbing the sys_id of an Incident

Now we can pop over to the background script processor and enter our code, using the sys_id that we pulled from the selected Incident.

Testing using a background script

After running the script, we can return to the Incident to verify that the file from the specified URL is now attached to the Incident.

The selected test Incident with the remote file attached

Just to make sure that all went well, you can click on the download link to pull down the attachment and look it over, verifying that it came across complete and intact. That pretty much demonstrates that it all works as we had intended. If you would like a copy to play around with on your own, you can pick it up here.

Hacking the Scripted REST API, Part II

“If you find a path with no obstacles, it probably doesn’t lead anywhere.”
Frank A. Clark

Last time, we built a Scripted REST API with a single Scripted REST Resource. To finish things up, we just need to add the actual script to the resource. To process our legacy support form, create an Incident, and respond to the user, the script will need to handle the following operations:

  • Obtain the input form field values from the request,
  • Format the data for use in creating the Incident,
  • Locate the user based on the email address, if one exists,
  • Create the Incident, and
  • Return the response page.

Obtain the input form field values from the request

This turned out to be much simpler than I first made it out to be. I knew that the out-of-the-box Scripted REST API was set up to handle JSON, both coming in and going out, and could also support XML, but I never saw anything regarding URL encoded form fields, so I assumed I would have to parse and unencode the request body myself. The problem that I was having, though, was getting the actual request body. I tried request.body, request.body.data, request.body.dataString, and even request.body.dataStream — nothing produced anything but null or errors. Then I read somewhere that that the Scripted REST API treats form fields as if they were URL query string parameters, and lumps them all together in a single map: request.queryParams. Once I learned that little tidbit of useful information, the rest was easy.

// get form data from POST
var formData = request.queryParams;
var name = formData.name[0];
var email = formData.email[0];
var short_description = formData.title[0];
var description = formData.description[0];

It did take me a bit to figure out that the values returned from the map are arrays and not strings, but once that became clear in my testing, I just added an index to the form field name and everything worked beautifully.

Format the data for use in creating the Incident

This was just a matter of formatting a few of the incoming data values with labels so that anything that did not have a field of its own on the Incident could be included in the Incident description.

// format the data
var full_description = 'The following issue was reported via the Legacy Support Form:\n\n';
full_description += 'Name: ' + name + '\n';
full_description += 'Email: ' + email + '\n\n';
full_description += 'Details:\n' + description;

Locate the user based on the email address

This doesn’t really have much to do with hacking the Scripted REST API, but I threw it in just as an example of the kind of thing that you can do once you have some data with which to work. In this case, we are simply using the email address that was entered on the form to search the User table to see if we have a user with that email. If we do, then we can use as the Caller on the Incident.

// see if we have a user on file with that email address
var contact = null;
var userGR = new GlideRecord('sys_user');
if (userGR.get('email', email)) {
	contact = userGR.getUniqueValue();
}

Create the Incident

This part is pretty vanilla as well.

// create incident
var incidentGR = new GlideRecord('incident');
if (contact) {
	incidentGR.caller_id = contact;
} else {
	incidentGR.caller_id.setDisplayValue('Guest');
}
incidentGR.contact_type = 'self-service';
incidentGR.short_description = short_description;
incidentGR.description = full_description;
incidentGR.assignment_group.setDisplayValue('Service Desk');
incidentGR.insert();
var incidentId = incidentGR.getDisplayValue('number');

The last line grabs the Incident number from the inserted Incident so that we can send that back to the user on the response page.

Return the response page

Now that we have complete all of the work, the last thing left to do is to respond to the user. Again, since we are not using the native JSON or XML formats, we are going to have to do some of the work a little differently than the standard Scripted REST API. Here is the working code:

// send response page
response.setContentType('text/html');
response.setStatus(200);
var writer = response.getStreamWriter();
writer.writeString(getResponsePageHTML(incidentId));

The first thing that you have to know is that you must set the content type and status before you get the stream writer from the response. If you don’t do that first, then things will not work. And even though you clicked the override checkbox and specified text/html as the format in the Scripted REST API definition, you still have to set it here as well. But once you do all of that, and do it in the right sequence, it all works great.

The response string itself is just the text of a standard HTML page. I encapsulated my sample page into a function so that it could be easily replaced with something else without disturbing any of the rest of the code. The sample version that I put together looks like this:

function getResponsePageHTML(incidentId) {
	var html = '';

	html += '<html>';
	html += ' <head>';
	html += '  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">';
	html += '  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>';
	html += ' </head>';
	html += ' <body>';
	html += '  <div style="padding: 25px;">';
	html += '   <h2>Thank you for your input</h2>';
	html += '   <p>We appreciate your business and value your feedback. Your satisfaction is of the utmost concern to us.</p>';
	html += '   <p>Your issue has been documented and one of our marginally competent technicians will get back to you within a few months to explain to you what you have been doing wrong.</p>';
	html += '   <p>Your incident number is ' + incidentId + '.</p>';
	html += '  </div>';
	html += ' </body>';
	html += '</html>';

	return html;
}

That’s it for the coding in ServiceNow. To see how it all works, we just need to point our old form to our new form processor after which we can pull up the modified form in a browser and give it a try. To repoint the legacy form, pull it up in a text editor, find the form tag, and enter the path to our Scripted REST API in the action attribute:

<form action="https://<instance>.service-now.com/api/<scope>/legacy/support" method="post">

With that little bit of business handled, we can pull up the form, fill it out, click on the submit button, and if all goes well, be rewarded for our efforts with our HTML response, including the number of the newly created Incident:

HTML response from the Scripted REST API form processor

Just to be sure, let’s pop over to our instance and check out the Incident that we should have created.

Incident created from submitting the legacy form

Well, that’s all there is to that. We built a Scripted REST API that accepted standard form fields as input and responded with a standard HTML web page, and then pointed an old form at it to produce an Incident and a thank you page. All in all, not a bad little perversion of the REST API feature of the tool!