Customizing the Data Table Widget, Part V

“Quality is not an act; it is a habit.”
Aristotle

In order to test my Data Table buttons and icons, I’m going to need a way to trigger both options, navigating to a new page and processing the global broadcast. Since my initial test page was configured to use the sys_user table, bouncing over to the User Profile page seems like easiest thing to do to demonstrate the first. But, to demonstrate the second, I’m going to have to create another widget, one that I will build just to prove that the other option works as well. Before we do that, though, let’s set up the button configuration JSON object to create one button and one icon, and have one implement the first option and the other implement the second. That will set things up for our testing.

[
   {
      "name":"button",
      "label":"Button",
      "heading":"Button",
      "color":"primary",
      "page_id":"user_profile",
      "hint":"Clicking this button should take you to the User Profile page"
   },{
      "name":"icon",
      "label":"Icon",
      "heading":"Icon",
      "icon":"user",
      "hint":"Clicking this icon should open a modal pop-up"
   }
]

We can enter that configuration using the Page Designer and clicking on the Edit icon (pencil) to bring up the options screen:

Entering the button configuration in the widget options panel

With that configured, we can actually test the button right away, as that one is configured to branch to another page (user_profile). We can’t test the icon, though, until we build a widget to listen for the broadcast.

So, I went over to the Service Portal Widgets list and clicked on the New button to create a net new widget to listen for the broadcast message. The only things this widget will do is listen and take action, so I didn’t need a Body HTML template and I didn’t need a Server script, so I just left those blank. For the Client controller, I just entered this:

function(spModal, $rootScope) {
	var c = this;
	$rootScope.$on('button.click', function(e, parms) {
		spModal.open({widget: 'user-profile', widgetInput: {sys_id: parms.record.sys_id}}).then(function() {
			//
		});
	});
}

This was about a simple as I could think of and still test out the process. Unfortunately, when I tested it, it didn’t work. It turns out that the stock user-profile widget cannot accept a sys_id from input. Well, that’s easily enough fixed, but I had to clone the user-profile widget to add in the code, and then I had to have my new listener widget launch the cloned snh-user-profile widget instead of the original. Now, that worked.

Modal pop-up opened by example listener widget

The listener widget has no HTML body, so you can pretty much drag it anywhere on the page. Once it shares the page with the customized Data Table widget, it will be able to pick up the broadcast and do whatever it is that you want to do. My example just checks for a ‘button-click’ event, but if you have more than one button on your page, you may also what to check to see which button was clicked before you take any action. I’ll leave that to those of you who want to try all this out on your own.

One unfortunate bit of bad news, though: in the process of testing all of this out, I clicked on one of the column headings to sort the list and my buttons disappeared. That was deflating! That’s why we pull on all of the levers and twist all of the dials, though. It’s important to check everything out thoroughly. Still, I hate to be reminded that I don’t really know what I am doing. It will probably take me a while to dig around, find the source of the problem, and come up with a viable solution. Still, I did promise to publish an Update Set soon, so I think I am going to go ahead and do that, even though this version violates Rule #1. If you don’t mind playing around with something that is obviously broken, you can get the version 1.0 Update Set here. Just be aware that there is a flaw that has yet to be corrected. Speaking of which, I better get busy diagnosing and correcting that problem.

Customizing the Data Table Widget, Part IV

“All life is an experiment. The more experiments you make, the better.”
Ralph Waldo Emerson

Now that we’ve tweaked the Data Table widget to support reference links and action buttons and icons, it’s time to add the code that will process the clicks on the buttons and icons. There are a number of things that we could do here, but since I like to start out small and build things up over time, today I think I will just handle a couple of the options: 1) navigating to another page, and 2) broadcasting the click and letting some other widget or function take it from there. The first is basically just a copy of what is already being done for the primary row link and the reference links and the second is just the weasel way out of building other options into the Data Table widget itself. By broadcasting the details of the click, users can pretty much do whatever it is that they would like to do by setting up listeners for the broadcast, and then I won’t have to add any additional logic or options on my end.

But before we get into all of that, I do have to admit that I got a little sidetracked the other day and started to tinker. Since I had already opened up the code and was messing with it anyway, I decided to go ahead and see if I could add a user avatar to any reference to the sys_user table. I had to dig around a little bit to see how that was done, but it is actually pretty simple with the sn-avatar tag.

Custom Data Table with User Avatars and Presence

I had to play around with the classes for various things to get everything to come out visually the way that I wanted, but the end result turned out to be a relatively simple addition to the HTML:

<td ng-repeat="field in ::data.fields_array" ng-if="!$first" aria-label="{{item[field].display_value}}" ng-class="{selected: item.selected}" data-field="{{::field}}" data-th="{{::data.column_labels[field]}}">
  <sn-avatar ng-if="item[field].value && item[field].type == 'reference' && item[field].table == 'sys_user'"  primary="item[field].value" class="avatar-small" show-presence="true" enable-context-menu="false"></sn-avatar>
  <a ng-if="item[field].value && item[field].type == 'reference'" href="javascript:void(0)" ng-click="go(item[field].table, {sys_id: item[field].value, name: item[field].display_value})" title="${Click for more on }{{::item[field].display_value}}">{{::item[field].display_value}}</a>
  <span ng-if="!item[field].value || item[field].type != 'reference'">{{::item[field].display_value}}</span>
</td>

Now, back to our regularly scheduled programming …

In the main Data Table widget’s Client Controller, I virtually copied the existing go function and then just added in a quick loop to hunt through the configured buttons to find the one that was clicked so that we could pass it on. Other than that, it’s virtually the same as the original:

$scope.buttonclick = function(button, item) {
	spNavStateManager.onRecordChange(c.data.table).then(function() {
		var parms = {};
		parms.table = c.data.table;
		parms.sys_id = item.sys_id;
		parms.record = item;
		for (var b in c.data.buttons) {
			if (c.data.buttons[b].name == button) {
				parms.button = c.data.buttons[b];
			}
		}
		$scope.ignoreLocationChange = true;
		for (var x in c.data.list) {
			c.data.list[x].selected = false;
		}
		item.selected = true;
		$scope.$emit(eventNames.buttonClick, parms);
	}, function() {
		// do nothing in case of closing the modal by clicking on x
	});	
};

Then, on the wrapper widgets, I copied the original listener and hacked it up to look into the button spec to see if there was a page configured. If so, then I navigate to the page; otherwise, I simply broadcast the click on the root scope so that another, independent widget can pick things up from there:

$scope.$on('data_table.buttonClick', callButtonClick);
	
function callButtonClick(e, parms) {
	if (parms.button.page_id) {
		var oid = $location.search().id;
		var p = parms.button.page_id;
		var s = {id: p, table: parms.table, sys_id: parms.sys_id, view: $scope.data.view};
		var newURL = $location.search(s);
		spAriaFocusManager.navigateToLink(newURL.url());
	} else {
		$rootScope.$broadcast('button.click', parms);
	}
}

That was all there was to it. Now all that’s left is to write a sample independent widget to listen for the broadcast and test all of this out to make sure that it all works. That should wrap all of this up quite nicely and should turn out to be the final installment in this particular series.

Customizing the Data Table Widget, Part III

“Many of life’s failures are people who did not realize how close they were to success when they gave up.”
Thomas Alva Edison

In addition to the reference links that I wanted to add to my version of the Data Table widget, I also wanted to add the capability to add buttons or icons to each row for specific actions. This seemed like something that could be accomplished relatively easily, and configured just like the many other options associated with the various flavors the Data Table. My thought was to be able to configure things to look something like this:

Data Table with example button and action icon

For the icons, my plan was to just leverage the Retina Icons that I had stumbled across the other day, and then lift the associated HTML right out of the old snh-form-field tag. But first, I had to come up with a way to pass in all of the information needed to configure each button. There is a way to use a table as a source for complex widget options, but this was just an experiment at this point, so I decided to go the quick and easy way and just pass in a JSON array of button specification objects that would look something like this:

[
  {
    "name": "button",
    "label": "Button",
    "heading": "Button",
    "color": "primary",
    "hint": "Click this button to do something",
    "action": "doSomething"
  },{
    ... etc ...
  }
]

I haven’t worked out all of the details yet, but you get the idea. That’s the basic concept, anyway … all we have to do now is to code it up!

The first order of business is to create the new widget option to hold the button specifications. One quick way to do that is to just edit the Option Schema directly, as it just happens to be a JSON object itself. In fact, there is already one option defined (enable_filter), so it is a simple matter to just use that one as an example and create a second one for our purposes:

[
   {
      "hint":"If enabled, show the list filter in the breadcrumbs of the data table",
      "name":"enable_filter",
      "default_value":"false",
      "section":"Behavior",
      "label":"Enable Filter",
      "type":"boolean"
   },{
      "hint":"A JSON object containing the specification for row-level buttons and action icons",
      "name":"buttons",
      "default_value":"",
      "section":"Behavior",
      "label":"Buttons",
      "type":"String"
   }
]

Once we have updated the Option Schema, editing an instance of the widget will include a place to enter the specifications for any desired buttons. Now we have to add code to the widget’s Server Script to pull in the value of the new option and turn it from a String to an Object. Since there is no guarantee that the instance author will provide a parsable JSON object, we will have to put in a little defensive code to check for a few possibilities.

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

And finally, we have to alter the HTML to include any configured buttons or icons. Modifications will need to be made in two places, one for the column headings and the other for the data columns. Here is what we will add for the column headings:

<th ng-repeat="button in data.buttons" class="text-nowrap center" tabindex="0">
  {{button.heading || button.label}}
</th>

And this is what we will toss in for each row of data:

<td ng-repeat="button in data.buttons" class="text-nowrap center" tabindex="0">
  <a ng-if="!button.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonclick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">{{button.label}}</a>
  <a ng-if="button.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonclick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">
    <span class="icon icon-{{button.icon}}" aria-hidden="true"></span>
    <span class="sr-only">{{button.hint}}</span>
  </a>
</td>

That puts everything on the screen and clickable, but we still some code to perform whatever action each button is intended to perform. That’s a tad bit more complicated, so I think we’ll tackle that the next time out ….

Customizing the Data Table Widget, Part II

“It always seems impossible until it’s done.”
Nelson Mandela

Today I am going continue on with my little Data Table widget customization by tackling the HTML portion of project, and then set things up so that we can do a little testing. Currently, the HTML that displays a single row in the data table does so in a way that the entire row is the clickable link to the details for that row. My intent is to change that so that the first column is the link to the details for that row, and then any other column that contains a reference field can be a link to the details of that specific reference. So, let’s take a look at the HTML structure that is there now:

<tbody>
  <tr ng-repeat="item in data.list track by item.sys_id">
    <td class="sr-only" tabindex="0" role="link" ng-click="go(item.targetTable, item)" aria-label="${Open record}"></td>
    <td aria-label="{{item[field].display_value}}" class="pointer sp-list-cell" ng-class="{selected: item.selected}" ng-click="go(item.targetTable, item)" ng-repeat="field in ::data.fields_array" data-field="{{::field}}" data-th="{{::data.column_labels[field]}}">{{::item[field].display_value}}</td>
  </tr>
</tbody>

I’m not quite sure what purpose is served by that first <TD> that doesn’t repeat, but there is a column heading to match, although neither appear to have any value or content. Maybe it’s a spot for a row-level checkbox or a glyph or something, but I’m not really smart enough to figure it out. The first thing that I did on my version was to delete it, along with the associated column heading — if I don’t understand why it needs to be there, then it just needs to go. Of course, that has nothing to do with what I am trying to accomplish, but if you happen to notice it gone in my version, that’s because I made it gone. I may end up having to go out and look for it one day when I finally figure out its value, but for now at least, it sleeps with the fishes.

With that out of the way, we can focus on the actual table columns, which are all treated the same in this version. The entire cell, not just the content, is the link to further information on the row, and with every column the same, no matter where you click on the row, the result is the same. We don’t want to do that in our version, though, so we can keep the ng-repeat on the <TD>, but the rest will be dependent on the column. Since we want the links to be on the content and not on the entire cell, we don’t really need anything else at the <TD> level anyway.

The first column will always be a link, and it will perform the same function as clicking anywhere on the row in the original version. For all other columns, they will also be links if the data type is reference, and there is a value. We can use the $first property to detect the first column, so the code for that link can basically be lifted from the original, with the addition of an ng-if to target the first column.

<a ng-if="$first" href="javascript:void(0)" ng-click="go(item.targetTable, item)" title="${Click for more on }{{::item[field].display_value}}">{{::item[field].display_value}}</a>

The remaining columns will require a few more lines. Not only do we need to determine that this not the first column, we also need to check to see of the type is reference, and if there is a value (if there is no value, then there is no need to code the link). Wrapping the whole thing in a <SPAN> will allow us to separate all of this from the first column, and then we can use an anchor tag for the links and a simple <SPAN> for the rest.

<span ng-if="!$first">
  <a ng-if="item[field].value && item[field].type == 'reference'" href="javascript:void(0)" ng-click="go(item[field].table, item[field].record)" title="${Click for more on }{{::item[field].display_value}}">{{::item[field].display_value}}</a>
  <span ng-if="!item[field].value || item[field].type != 'reference'">{{::item[field].display_value}}</span>
</span>

At this point, I am leveraging the existing go function to handle the clicks. That function takes a table and a record as an argument, so I figure that I can just pass in the name of the referenced table and a mocked-up record (more on that later) and simply reuse the existing function. I may want to rethink that later and create a function specifically for reference field clicks, but for now this works, so it’s good enough. My earlier server-side additions did not originally create mocked-up record for each reference field, so I had to go back and that in to bring all of this together. I added that to the same block of code where I was adding the name of related table.

if (record[fld].type == 'reference') {
	record[fld].table = gr.getElement(fld).getED().getReference();
	record[fld].record = {sys_id: {value: record[fld].value, display_value: record[fld].value}, name: {value: record[fld].display_value, display_value: record[fld].display_value}};
}

Well, that should do it — at least, for this initial version. Now we just need to take it out for a spin and make sure that everything works. For that, we’ll need to create a test page and then put the widget on the page and configure it. Then we can hit the Try It! button.

First test of the customized Data Table widget

Well, that wasn’t too bad. Everything seems to work as I had intended. Nothing happens right now when you click on anything, but that’s because the go function simply broadcasts the click events, and I don’t have anything listening for that in this version. This was a clone of the core Data Table widget, and the listeners are in the wrapper widgets that implement the various ways to configure the display and contents. I’ll have to clone those wrapper widgets as well, or maybe modify them to choose between the stock Data Table widget and my customized version. Either one seems like a lot more work than I want to dive into right now, so I’m going to leave that for a later installment.

Customizing the Data Table Widget

“The reasonable man adapts himself to the world: the unreasonable one persists in trying to adapt the world to himself. Therefore, all progress depends on the unreasonable man.”
George Bernard Shaw

After spending some time playing around with my Data Table Widget Content Selector, I realized that there were a few things that I wanted out of the Data Table widget that just weren’t on its list of features. For one thing, the entire row was the source of the link to further details. In the standard ServiceNow UI, the first column on any given list is the link to further details on the subject of the the row, and links in other columns took you to details related to that specific column. On a list of Incidents, for example, clicking on the link on the first column will take you to the Incident, but clicking on a link in any other column, say Assignment Group or Location, will bring up information on the Assignment Group or Location. I wanted to have a Service Portal list that behaved in the same manner. I started rooting around in the code for the Data Table and realized that this would be more than a simple hack, so I decided to clone the widget and create my own SNH Data Table.

One thing that I discovered while playing around with my copy of the Data Table widget was that there was a minor bug in widget related to the fields option, which was the subject of my earlier hack of the Data Table from URL definition widget. Throughout the widget, this option is referenced as fields, but in the actual widget options, it is named field_list. The statement that copied fields from the options didn’t really copy anything, as the actual data was stored under the variable name field_list. So the first thing that I ended up doing with my copy was to change this:

if (!data.fields) {
	if (data.view)
		data.fields = $sp.getListColumns(data.table, data.view);
	else
		data.fields = $sp.getListColumns(data.table);
}

… to this:

if (!data.fields) {
	if (data.field_list) {
		data.fields = data.field_list;
	} else if (data.view) {
		data.fields = $sp.getListColumns(data.table, data.view);
	} else {
		data.fields = $sp.getListColumns(data.table);
	}
}

That seemed to have rectified that little shortcoming, so now back to my intended purpose, which was to provide column-level links rather than the current row-level link. First things first: I needed to find out if we had enough information on hand to provide the links, or if we needed to add some code to gather up more details for columns that could contain a link. Unfortunately, the code the code that gathers up the row data doesn’t reveal much detail on what, exactly, is gathered up for each field in each row:

while (gr._next()) {
	var record = {};
	$sp.getRecordElements(record, gr, data.fields);
	. . .
}

I would have to dig out the code for $sp.getRecordElements() to know what data was being pulled for each field, which sounded a lot like a major hunting expedition. Instead, I took the lazy way out (my favorite!), and just dumped out the result with a gs.info(JSON.Stringify(record)); statement right after the $sp.getRecordElements(). After that, all I had to do was to bring up a list using the widget and then dig through syslog.list. What I learned was that each field in the record object contained a type, a value, and a display_value, but not the name of the table for the reference fields, which I would need if I was going to create a link. So, I needed to add a little code to pick up that extra bit of info for each field where type=reference. There was actually some similar code right above where I needed to insert my own, so what I ended up adding turned out to be a bastardized copy of the preceeding logic:

for (var f in data.fields_array) {
	var fld = data.fields_array[f];
	if (gr.isValidField(fld)) {
		if (record[fld].type == 'reference') {
			record[fld].table = gr.getElement(fld).getED().getReference();
		}
	}
}

Basically, we just loop through all of the fields in the record, look for those where type=reference and then go fetch the reference table from the source GlideRecord. Now that we have everything that we need in the underlying data, the last thing that we need to do will be to alter the HTML to provide individual column links instead of a single link for the entire row. That seems like a good place to start, next time out

Configurable Data Table Widget Content Selector, Part II

“Code reuse is the Holy Grail of Software Engineering.”
Douglas Crockford

So far, I’ve hacked up one of the stock Data Table widgets and created one example of a configuration script for my proposed Data Table Content Selector. Now it’s time to actually build the widget itself. Here is how I envision it appearing based on the sample configuration that I put together earlier:

List Content Selector based on earlier configuration

Basically, it contains individual sections for each of the three configurable selections: Perspective, State, and Table. Different configurations would, of course, produce different results, but the concept remains the same regardless of your particular configuration: you make a selection using the widget and then the widget will construct the appropriate URL based on your selection and then take you to that URL. The hacked Data Table from URL Definition widget, which shares the page, will then pick up those URL parameters, which will in turn drive the records that appear in the Data Table. Here is one possible arrangement where the Data Table widget consumes 75% of the screen and the companion selector widget occupies the remaining 25% on the right hand side.

One potential arrangement of the two widgets on the page

Where you place your new widget is entirely up to you. The example above happens to use a 9/3 split to put the selector on the right, but you could also use a 3/9 split to put it on the left, or if you did not want to take away from the width of the Date Table, you could place it above, or below (not recommended), or even put it into some kind of modal pop-up box accessed via a simple link or button. The point is, you can put it wherever you want and it won’t affect the way that it works. Although the two widgets do share the same portal page, they don’t actually communicate with each other directly. There is no broadcasting or listening for messages between the two. The selector simply constructs the appropriate URL and then both widgets draw from the URL parameters to determine what ends up on the screen. Here is another example with a different arrangement and a different configuration:

Another potential arrangement of the two widgets on the page

In order to handle all possible situations, there has to be some default behavior in the event that no URL parameters are provided. Based on the configuration file, default values can be determined for all three selections (perspective, state, and table), and there can even be a default response for an invalid configuration. No matter what the circumstances, you want the page to do something so things are not left broken. To retain the order, configuration parameters are specified in arrays rather than objects. The default then, can always be the first item in the array. For the perspective, which is role based, it would be the first item in the array for which the current user has an applicable role. For tables, which can be different for different perspectives, it would be the first item on the list for the selected perspective. If defaults cannot be determined based on the configuration and the current user, then the appropriate action would be to leave the page entirely and go to some other page such as the home page of the portal. This is preferable to leaving the page up in a broken state.

The client side code, which handles the construction of the new URL, includes all of the code necessary to process both the default responses and the user’s selections:

function($scope, $location) {
	var c = this;

	if (!c.data.table) {
		if (c.data.config.defaults.table) {
			refreshPage(c.data.config.defaults.table, c.data.config.defaults.perspective, c.data.config.defaults.state);
		} else {
			window.location.search = '';
		}
	}

	$scope.selectTable = function(selection) {
		refreshPage(selection, c.data.perspective, c.data.state);
	};

	$scope.selectPerspective = function(selection) {
		refreshPage(c.data.config.table[selection][0].name, selection, c.data.config.state[0].name);
	};

	$scope.selectState = function(selection) {
		refreshPage(c.data.table, c.data.perspective, selection);
	};

	function refreshPage(table, perspective, state) {
		var tableInfo = getTableInfo(table, perspective);
		var newSearch = '?id=' + $location.search()['id'];
		newSearch += '&table=' + tableInfo.name;
		newSearch += '&filter=' + tableInfo[state].filter;
		newSearch += '&fields=' + tableInfo[state].fields;
		newSearch += '&px=' + perspective;
		newSearch += '&sx=' + state;
		window.location.search = newSearch;
	}

	function getTableInfo(table, perspective) {
		var tableInfo = {};
		for (var i=0; i<c.data.config.table[perspective].length; i++) {
			if (c.data.config.table[perspective][i].name == table) {
				tableInfo = c.data.config.table[perspective][i];
			}
		}
		return tableInfo;
	}
}

At the top of the script, we verify that a table has been selected. If not, then we check to make sure that there is a default table. If there is, then we select the default table; otherwise, we return to the home page of the Portal. If a table has already been selected, then we just wait for the user to make another selection, at which time we refresh the page with the selected options.

The server side code is a little more involved. The first thing that we need to do is go out and get the configuration information. Once we have that in hand, we can determine the authorized perspectives, and once those have been established, we can then determine the defaults. After that, it’s just a matter of pulling in the URL parameters and verifying them against the configuration. The final action on the server side is to use the defined filter for the active state to get a row count for every applicable table.

(function() {
	var mdc = new MyDataConfig();
	data.config = mdc.getConfig($sp);
	data.config.authorizedPerspective = getAuthorizedPerspectives();
	establsihDefaults();
	
	if (!input) {
		data.list = [];
		if (data.config.defaults.perspective && $sp.getParameter('table')) {
			data.perspective = data.config.defaults.perspective;
			data.state = data.config.defaults.state;
			data.table = data.config.defaults.table;
			if ($sp.getParameter('px')) {
				for (var i=0; i<data.config.authorizedPerspective.length; i++) {
					if ($sp.getParameter('px') == data.config.authorizedPerspective[i].name) {
						data.perspective = $sp.getParameter('px');
					}
				}
			}
			for (var i=0; i<data.config.table[data.perspective].length; i++) {
				if ($sp.getParameter('table') == data.config.table[data.perspective][i].name) {
					data.table = $sp.getParameter('table');
				}
			}
			if ($sp.getParameter('sx')) {
				for (var i=0; i<data.config.state.length; i++) {
					if ($sp.getParameter('sx') == data.config.state[i].name) {
						data.state = $sp.getParameter('sx');
					}
				}
			}
			for (var i in data.config.table[data.perspective]) {
				getValues(data.config.table[data.perspective][i]);
			}
		}
	}
	
	function getAuthorizedPerspectives() {
		var authorizedPerspective = [];
		for (var i in data.config.perspective) {
			var p = data.config.perspective[i];
			if (p.roles) {
				var role = p.roles.split(',');
				var authorized = false;
				for (var i=0; i<role.length; i++) {
					if (gs.hasRole(role[i])) {
						authorized = true;
					}
				}
				if (authorized) {
					authorizedPerspective.push(p);
				}
			} else {
				authorizedPerspective.push(p);
			}
		}
		return authorizedPerspective;
	}
	
	function establsihDefaults() {
		data.config.defaults = {};
		if (data.config.authorizedPerspective[0]) {
			data.config.defaults.perspective = data.config.authorizedPerspective[0].name;
			for (var i in data.config.state) {
				var s = data.config.state[i];
				if (!data.config.defaults.state) {
					data.config.defaults.state = s.name;
				}
			}
			for (var i in data.config.table[data.config.defaults.perspective]) {
				var t = data.config.table[data.config.defaults.perspective][i];
				if (!data.config.defaults.table) {
					data.config.defaults.table = t.name;
				}
			}
		}
	}
	
	function getValues(tableInfo) {
		var gr = new GlideRecord(tableInfo.name);
		gr.addEncodedQuery(tableInfo[data.state].filter);
		gr.query();
		if (gr.getRowCount() > 0 || tableInfo.name == data.table) {
			data.list.push({
				name: tableInfo.name,
				label: gr.getLabel(),
				value: gr.getRowCount()
			});
		}
	}
})();

That’s all there is to it. For those of you who like to follow along at home, I’ve bundled all of the parts, including the hacked Data Table from URL Definition widget, into an Update Set so that you can give things a spin in your own environment. If you have any ideas for improvements, please feel free to leave them in the Comments — thanks!

Configurable Data Table Widget Content Selector

“I like being a beginner. I like the moment where I look at everyone and say, ‘I have no idea how to do this, let’s figure it out.'”
— Jon Acuff, Do Over: Rescue Monday, Reinvent Your Work, and Never Get Stuck

The reason that I needed to hack the out-of-the-box Data Table from URL Definition widget was because I had this vision of creating a small companion widget that could share the same page and allow a user to select what data they wanted to be displayed in the data table. To achieve my vision, I needed to be able to pass three elements in via the URL: the table, the filter, and the columns to display. The stock widget already supported the table and the filter, but the columns are controlled by the view, which also works to some extent, but for every desired selection/arrangement of columns, you would have to define a new view. That seemed like a little more work than I wanted to get into, so I tweaked the stock widget just a tad to allow me to simply pass in the names of the columns that I wanted to display in the order that I wanted to display them.

The Concept

The basic idea for my new widget is that it would allow the user to view their personal data in a variety of contexts by selecting one or more configurable parameters, and based on those parameters, produce a URL that would be consumed by the Data Table from URL Definition widget, which would do the heavy lifting of displaying the information. This companion widget could sit above or to the side of the main data table and provide a means for the user to control what information is displayed in the main content area. My hope was that I could create something completely driven off of an external configuration file, and customization and personalization could be handled entirely by altering the configuration file without having to crack open the widget itself. I saw my widget providing the user selectable options in three distinct areas: perspective, state, and table.

Perspective

The first option would be perspective, and would be the highest order in the hierarchy of choices. Other available options would be based on the selected perspective. The perspective represents the vantage point of the user. For example, in my little demonstration configuration, I have provided two perspective options for viewing data related to Incidents and Requests: Requester and Fulfiller. This is just one example. If you were looking at Project data, you might want perspectives such as Executive Sponsor, Project Manager, and Project Team. If you were looking at Change data, you might want something like Change Coordinator, Change Manager, and Change Implementer. Or if you wanted to look at work queues, your perspectives might be something like My Work and My Group’s Work. The point is that the widget should be designed in such a way that you can come up with whatever perspectives your particular situation might require, and all you should have to do is provide the appropriate configuration.

Each perspective will have three properties: name, label, and roles. The name and label properties are pretty self-explanatory; the roles property provides a means to limit the exposure of certain perspectives to just those folks for whom that perspective would be appropriate. In my example, everyone has access to the requester perspective, so there are no roles associated with that one. The fulfiller perspective, however, is only appropriate for those individuals involved in request fulfillment, so that perspective is linked to the itil role. Individuals who have the itil role will see the option for the fulfiller perspective and those that do not will not. Here is my example perspective configuration:

perspective: [{
	name: 'requester',
	label: 'Requester',
	roles: ''
},{
	name: 'fulfiller',
	label: 'Fulfiller',
	roles: 'itil'
}]

State

The second option would be state, which provides a means to classify the records from the selected table. For my example, I have defined 4 unique states: Open, Closed, Recent, and All. As with the example perspectives, the example configuration that I have chosen for my demonstration configuration represents just one possibility. You might prefer states such as Submitted, Approved, Assigned, and Completed. The intent is to set things up in such a way that you can configure any state options that are appropriate for your use case. The whole point is to create a widget in such a way that you can configure it in accordance with your own tastes and desires, and do so without having to alter the actual widget itself. Here is my example state configuration:

state: [{
	name: 'open',
	label: 'Open'
},{
	name: 'closed',
	label: 'Closed'
},{
	name: 'recent',
	label: 'Recent'
},{
	name: 'all',
	label: 'All'
}]

Table

The third option would be table, which is simply the name of table from which the records will be retrieved. Unlike the perspective and state options, the table option is actually shared with the Data Table from URL Definition widget. With our recent modification, the Data Table from URL Definition widget will now accept three URL parameters: table, filter, and fields. The table value will come directly from the table selected on our new companion widget. The filter and fields value will come from the filter and fields values defined in the configuration for the selected perspective, state, and table selected. For each defined perspective, there will be a configuration for one or more tables. For each of the specified tables, there will be filter and fields values for each defined state. To build the URL for our new page containing both the Data Table from URL Definition widget and our new content controller widget, we will use the selected perspective, state, and table to add URL parameters for perspective, state, and table directly from the selections, plus filter and fields, determined based on the values in the configuration file for the selected perspective, state, and table.

All together, including a couple of table definitions for the Requester and another for the Fulfiller, the complete example configuration looks like this:

var MyDataConfig = Class.create();
MyDataConfig.prototype = {

	initialize: function() {
	},

	perspective: [{
		name: 'requester',
		label: 'Requester',
		roles: ''
	},{
		name: 'fulfiller',
		label: 'Fulfiller',
		roles: 'itil'
	}],

	state: [{
		name: 'open',
		label: 'Open'
	},{
		name: 'closed',
		label: 'Closed'
	},{
		name: 'recent',
		label: 'Recent'
	},{
		name: 'all',
		label: 'All'
	}],

	table: {
		requester: [{
			name: 'incident',
			open: {
				filter: 'caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Eactive%3Dtrue',
				fields: 'number,opened_by,opened_at,short_description'
			},
			closed: {
				filter: 'caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Eactive%3Dfalse',
				fields: 'number,opened_by,opened_at,closed_at,short_description'
			},
			recent: {
				filter: 'caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Esys_created_on%3E%3Djavascript%3Ags.beginningOfLast30Days()',
				fields: 'number,state,opened_by,opened_at,closed_at,short_description'
			},
			all: {
				filter: 'caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe',
				fields: 'number,state,opened_by,opened_at,closed_at,short_description'
			}
		},{
			name: 'sc_request',
			open: {
				filter: 'requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Eactive%3Dtrue',
				fields: 'number,opened_by,opened_at,short_description'
			},
			closed: {
				filter: 'requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Eactive%3Dfalse',
				fields: 'number,opened_by,opened_at,closed_at,short_description'
			},
			recent: {
				filter: 'requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Esys_created_on3E%3Djavascript%3Ags.beginningOfLast30Days()',
				fields: 'number,request_state,opened_by,opened_at,closed_at,short_description'
			},
			all: {
				filter: 'requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe',
				fields: 'number,request_state,opened_by,opened_at,closed_at,short_description'
			}
		}],
		fulfiller: [{
			name: 'incident',
			open: {
				filter: 'assigned_toDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Eactive%3Dtrue',
				fields: 'number,caller_id,opened_at,short_description'
			},
			closed: {
				filter: 'assigned_toDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Eactive%3Dfalse',
				fields: 'number,caller_id,opened_at,closed_at,short_description'
			},
			recent: {
				filter: 'assigned_toDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Esys_created_on3E%3Djavascript%3Ags.beginningOfLast30Days()',
				fields: 'number,state,caller_id,opened_at,closed_at,short_description'
			},
			all: {
				filter: 'assigned_toDYNAMIC90d1921e5f510100a9ad2572f2b477fe',
				fields: 'number,state,caller_id,opened_at,closed_at,short_description'
			}
		}]
	},

	getConfig: function(sp) {
		return {
			perspective: this.perspective,
			state: this.state,
			table: this.table
		};
	},

	type: 'MyDataConfig'
};

Now that we have the configuration all figured out, all we have to do is actually build the widget! Well, that seems like a good exercise for the next time out

Fun with the Service Portal Data List Widget

“I can’t do it never yet accomplished anything. I will try has performed miracles.”
George Burnham

Straight out of the box, the ServiceNow Service Portal comes bundled with a trio of widgets for displaying rows of a table: a primary data table widget and two additional wrapper widgets that provide different means to pass configuration parameters to the primary widget.

Service Portal data table widgets

One of the wrapper widgets is the Data Table from URL Definition widget, which was almost exactly what I was looking for. The problem, of course, was the almost. I needed something that was exactly what I was looking for. So close, but no cigar. The problem was that it took most, but not all, of the parameters that I wanted pass via the URL. You can pass in the name of the table, the name of the view, a query filter, and a number of other, related options, but you cannot pass in a list of columns that you want to have displayed in the list. There is a property called fields, which is set up for that very purpose, but its value is hard-coded rather than being pulled from the URL.

Well, that won’t work!

Here is the line in question:

data.fields = $sp.getListColumns(data.table, data.view);

Here is what I would like see on that line:

data.fields = $sp.getParameter('fields') || $sp.getListColumns(data.table, data.view);

That really shouldn’t hurt anything at all. All that would do would be to take a quick peek at the URL, and if someone provided a list of fields, then it would use that list; otherwise, it would revert to what it is currently doing right now. It would simply add what I wanted without taking away anything that it is already set up to do. Unfortunately, this particular widget is one of those provided in read-only mode and you are not allowed to modify it, even if you are an admin. Well, isn’t that restrictive!

The recommended course of action in these cases is to make a clone or copy of the protected widget under a new name and then modify that one, leaving the original intact. I thought about doing just that, but I’m not really one to blindly follow the recommended course of action at every turn. I just wanted to make this one small change to this one and leave it at that. Fortunately, there is a way to do just that. First, you have to export the widget to XML.

Exporting the widget to XML

Next, make whatever modifications that you want to make to the exported XML, being careful not to disturb anything else, and the save the updated XML. Now, go back to the list of widgets and use the hamburger menu on one of the list columns to select Import XML.

Importing the widget XML back into ServiceNow

Browse for your XML file, upload it, and now the modified widget is back where it belongs with your modification in place. Voila! Easy, peasy. Now, I can get back to doing what I wanted to do with this widget in the first place.