Aggregate List Columns

“Get a good idea and stay with it. Do it, and work at it until it’s done right.”
Walt Disney

We have had a lot of fun with the Service Portal Data Table Widget on this site. So much so, in fact, that we had to make our own copy to avoid extensive modifications to a core component of the Service Portal. So far, we have created a Configurable Data Table Widget Content Selector, allowed individual columns to be links to referenced records, added buttons and icons, added User Avatars to user columns, set up the Configurable Data Table Widget Content Selector to use a JSON object to configure the widget, created an Editor for the JSON configuration object, added check boxes to the rows, added an additional extension of the base Data Table Widget to use the JSON configuration object directly, and built a User Directory using all of these custom components. That’s quite a bit of customization, but there is at least one more thing that we could do to make this all even better.

The feature that I have in mind is to have one or more columns that would include counts of related records. For example, on a list of Assignment Groups, you might want to include columns for how many people are in the group or how many open Incidents are assigned to the group. These are not columns on the Group table; these are counts of related records. It seems as if we could borrow some of the concepts from our Buttons and Icons strategy to come up with a similar approach to configuring Aggregate List Columns that could be defined when setting up the configuration for the table. You know what I always like to ask myself: How hard could it be?

Let’s take a quick look at what we need to configure a button or icon using the current version of the tools. Then we can compare that to what we would need to configure an Aggregate List Column using a similar approach. Here is the data that we collect when defining a button/icon:

  • Label
  • Name
  • Heading
  • Icon
  • Color
  • Hint
  • Page

The first three would appear to apply to our new requirement as well, but the rest are specific to the button/icon configuration. Still, we could snag those first three, and copy all of the code that deals with those first three, and then ditch the rest and replace them with data points that will be useful to our new purpose. Our list, then, would end up looking something like this:

  • Label
  • Name
  • Heading
  • Table
  • Field
  • Filter

The first three would be treated exactly the same way that their counterparts are treated in the button/icon code. The rest would be unique to our purpose. Table would be the name of the table that contains the related records to be counted. Field would be the name of the reference field on that table that would contain the sys_id of the current row. Filter would be an additional query filter that would be used to further limit the records to be counted. The purpose for the Filter would be to provide for the ability to count only those records desired. For example, a Table of Incident and a Field of assigned_to would count all of the Incidents ever assigned to that person, which is of questionable value. With the ability to add a Filter of active=true, that would limit the count to just those Incidents that were currently open. That is actually a useful statistic to add to a list.

One other thing that would be useful would be the addition of a link URL that would allow you to pull up a list of the records represented in the count. Although I definitely see the value in this additional functionality, anyone who has followed this web site for any length of time knows that I don’t really like to get too wild and crazy right out of the gate. My intent is to just see if I can get the count to appear on the list before I worry too much about other features that would be nice to have, regardless of their obvious value.

So, it seems as if the place to start here would be to pull up a JSON configuration object and see if we can add a configuration for an Aggregate List Column. When we built the User Directory, we added a simple configuration file to support both the Location Roster and the Department Roster. This looks like a good candidate to use as a starting point to see if we can set up a test case. Here is the original configuration file used in the User Directory project:

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={{sys_id}}',
				fields: 'name,title,email,location',
				btnarray: [],
				refmap: {
					cmn_location: 'location_roster'
				},
				actarray: []
			},
			location: {
				filter: 'active=true^location={{sys_id}}',
				fields: 'name,title,department,email',
				btnarray: [],
				refmap: {
					cmn_department: 'department_roster'
				},
				actarray: []
			}
		}]
	},

	type: 'RosterConfig'
});

For our purpose, we don’t really need two state options, so we can simplify this even further by reducing this down to just one that we can call all. Then we can add our example aggregate configuration just above the button configuration. Also, since this is just a test, we will want to limit our list of people to just members of a single Assignment Group, so we can update the filter accordingly to limit the number of rows. Here is the configuration that I came up with for an initial test.

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

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

	state: [{
		name: 'all',
		label: 'All',
	}],

	table: {
		all: [{
			name: 'sys_user',
			displayName: 'User',
			all: {
				filter: 'active=true^sys_idIN46c4aeb7a9fe1981002bbd372644a37b,46d44a23a9fe19810012d100cca80666,5137153cc611227c000bbd1bd8cd2005,5137153cc611227c000bbd1bd8cd2007,9ee1b13dc6112271007f9d0efdb69cd0,f298d2d2c611227b0106c6be7f154bc8',
				fields: 'name,title,email,department,location',
				aggarray: [{
					label: 'Incidents',
					name: 'incidents',
					heading: 'Incidents',
					table: 'incident',
					field: 'assigned_to',
					filter: 'active=true'
				}],
				btnarray: [],
				refmap: {},
				actarray: []
			}
		}]
	},

	type: 'AggregateTestConfig'
});

For now, I just created a simple filter using all of the sys_ids of all of the members of the Hardware group rather than bringing in the group member table and filtering on the group itself. This is not optimum, but this is just a test, and it will avoid other issues that we can discuss at a later time. The main focus at this point, though is the new aggarray property, which includes all of the configuration information that we listed earlier.

aggarray: [{
   label: 'Incidents',
   name: 'incidents',
   heading: 'Incidents',
   table: 'incident',
   field: 'assigned_to',
   filter: 'active=true'
}]

Now that we have a configuration script, we can create a page and try it out, even though we have not yet done any coding to support the new configuration attributes. At this point, we just want to see if the list comes up, and since nothing will be looking for our new data, that portion of the configuration object will just be ignored. I grabbed a copy of the Location Roster page from the User Directory project and then cloned it to create a page that I called Aggregate Test. Then I edited the configuration for the table to change the Configuration Script value to our new Script Include, AggregateTestConfig. I also removed the value that was present in the State field, as we only have one state, so no value is needed.

Updating the Configuration Script on the cloned portal page

With that saved, we can run out to the Service Portal and pull up the new page and see how it looks.

First look at the new test page using our new configuration script

Well, that’s not too bad for just a few minutes of effort. Of course, that was just the easy part. Now we have to tinker with the widgets to actually do something with this new configuration data that we are passing into the modules. That’s going to be a bit of work, of course, so we’ll start taking a look at how we want to do that next time out.

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.

Content Selector Configuration Editor, Corrected

“Beware of little expenses; a small leak will sink a great ship.”
Benjamin Franklin

It just seems to be the natural order of things that just when I think I finally wrapped something up and tied a big bow on it, I stumble across yet another fly in the ointment. The other day I was building yet another configuration script using my Content Selector Configuration Editor, but when I tried to use it, I ran into a problem. Since I happened to be working in a Scoped Application, the generated script failed to locate the base class, since that class in the global scope. The problem was this generated line of script:

ScopedAppConfig.prototype = Object.extendsObject(ContentSelectorConfig, {

For a Scoped Application, that line should really be this:

ScopedAppConfig.prototype = Object.extendsObject(global.ContentSelectorConfig, {

It seemed simple enough to fix, but first I had to figure out how to tell if I was in a Scoped App or not. After searching around for a bit, I finally came across this little useful tidbit:

var currentScope = gs.getCurrentApplicationScope();

Once I had a way of figuring out what scope I was in, it was easy enough to change this line:

script += ".prototype = Object.extendsObject(ContentSelectorConfig, {\n";

… to this:

script += ".prototype = Object.extendsObject(";
if (gs.getCurrentApplicationScope() != 'global') {
	script += "global.";			
}
script += "ContentSelectorConfig, {\n";

Once I made the change, I pulled my Scoped Application script up in the Content Selector Configuration Editor, saved it, and tried it again. This time, it worked, which was great. Unfortunately, later on when I went to pull it up in the editor again to make some additional changes, it wasn’t on the drop-down list. It didn’t take long to figure out that the problem was the database table search filter specified in the pick list form field definition:

<snh-form-field
  snh-label="Content Selector Configuration"
  snh-model="c.data.script"
  snh-name="script"
  snh-type="reference"
  snh-help="Select the Content Selector Configuration that you would like to edit."
  snh-change="scriptSelected();"
  placeholder="Choose a Content Selector Configuration"
  table="'sys_script_include'"
  default-query="'active=true^scriptCONTAINSObject.extendsObject(ContentSelectorConfig'"
  display-field="'name'"
  search-fields="'name'"
  value-field="'api_name'"/>

The current filter picks up all of the scripts tht contain “Object.extendsObject(ContentSelectorConfig”, but not those that contained “Object.extendsObject(global.ContentSelectorConfig”. I needed to tweak the filter just a bit to pick up both. I ended up with the following filter value:

active=true^scriptCONTAINSObject.extendsObject(ContentSelectorConfig^ORscriptCONTAINSObject.extendsObject(global.ContentSelectorConfig

… and that took care of that little issue. I’m sure that this will not be the last time that I run into a little issue with this particular product, but for now, here is yet another version stuffed into yet another Update Set. Hopefully, that will be it for the needed corrections for at least a little while!

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.

Customizing the Data Table Widget, Again

“Work never killed anyone. It’s worry that does the damage. And the worry would disappear if we’d just settle down and do the work.”
Earl Nightingale

It’s been a while since I first set out to build a custom version of the stock Data Table widget, but since that time, I have used my hacked up version for a number of different projects, including sharing pages with my companion widget, the Configurable Data Table Widget Content Selector. Now that I have a way to edit the configuration scripts for the content selector, that has become my primary method for setting up the parameters for a data table. The content selector, though, was designed to give the end user the ability to select different Perspectives, different States, and different Tables. That’s a nice feature, but there are times when I just want to display a table of data without any options to look at any other data. I thought about setting up an option to make the content selector hidden for those instances, but then it occurred to me that the better approach was to cut out the middleman entirely and create a version of the Data Table widget that read the configuration script directly. This way, I wouldn’t have to put the content selector on the page at all.

So I cloned my SNH Data Table from URL Definition widget to create a new SNH Data Table from JSON Configuration widget. Then I opened up my Content Selector widget and started stealing parts and pieces of that guy and pasting them into my new Data Table widget, starting with the widget option for the name of the configuration script:

{
      "hint":"Mandatory configuration script that is an extension of ContentSelectorConfig",
     "name":"configuration_script",
     "section":"Behavior",
     "label":"Configuration Script",
     "type":"string"
}

I also threw in three new options so that you could override the default Perspective, Table, and State values.

{
   "hint":"Optional override of the default Perspective",
   "name":"perspective",
   "section":"Behavior",
   "label":"Perspective",
   "type":"string"
},{
   "hint":"Optional override of the default Table",
   "name":"table",
   "section":"Behavior",
   "label":"Table",
   "type":"string"
},{
   "hint":"Optional override of the default State",
   "name":"state",
   "section":"Behavior",
   "label":"State",
   "type":"string"
}

In the HTML section, I copied in the two warning messages and pasted them in with minimal modifications:

<div ng-hide="options && options.configuration_script">
  <div class="alert alert-danger">
    ${You must specify a configuration script using the widget option editor}
  </div>
</div>
<div ng-show="options && options.configuration_script && !data.config.defaults">
  <div class="alert alert-danger">
    {{options.configuration_script}} ${is not a valid Script Include}
  </div>
</div>

On the server side, I deleted the first several lines of code that dealt with grabbing the table and view from the URL and making sure that something was there, and replaced it with some code that I pretty much lifted intact from the content selector widget. Here is the code that I removed:

deleteOptions(['table','field_list','filter','order_by', 'order_direction','order','maximum_entries']);
if (input) {
	data.table = input.table;
	data.view = input.view;
} else {
	data.table = $sp.getParameter('table') || $sp.getParameter('t');
	data.view = $sp.getParameter('view');
}

if (!data.table) {
	data.invalid_table = true;
	data.table_label = "";
	return;
}

… and here is what I replaced it with:

data.config = {};
data.user = {sys_id: gs.getUserID(), name: gs.getUserName()};
if (options) {
	if (options.configuration_script) {
		var instantiator = new Instantiator(this);
		var configurator = instantiator.getInstance(options.configuration_script);
		if (configurator != null) {
			data.config = configurator.getConfig($sp);
			data.config.authorizedPerspective = getAuthorizedPerspectives();
			establishDefaults(options.perspective, options.table, options.state);
		}
	}
}

if (data.config.defaults && data.config.defaults.perspective && data.config.defaults.table && data.config.defaults.state) {
	var tableList = data.config.table[data.config.defaults.perspective];
	var tbl = -1;
	for (var i in tableList) {
		if (tableList[i].name == data.config.defaults.table) {
			tbl = i;
		}
	}
	data.tableData = tableList[tbl][data.config.defaults.state];
	data.table = data.config.defaults.table;
} else {
	data.invalid_table = true;
	data.table_label = "";
	return;
}

I also grabbed a couple of the functions that were called from there and pasted those in down at the bottom:

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 ii in role) {
				if (gs.hasRole(role[ii])) {
					authorized = true;
				}
			}
			if (authorized) {
				authorizedPerspective.push(p);
			}
		} else {
			authorizedPerspective.push(p);
		}
	}
	return authorizedPerspective;
}

function establishDefaults(perspective, table, state) {
	data.config.defaults = {};
	var p = data.config.authorizedPerspective[0].name;
	if (perspective) {
		if (data.config.table[perspective]) {
			p = perspective;
		}
	}
	if (p) {
		data.config.defaults.perspective = p;
		for (var t in data.config.table[p]) {
			if (!data.config.defaults.table) {
				data.config.defaults.table = data.config.table[p][t].name;
			}
		}
		if (table) {
			for (var t1 in data.config.table[p]) {
			if (data.config.table[p][t1].name == table) {
					data.config.defaults.table = table;
				}
			}
		}
		data.config.defaults.state = data.config.state[0].name;
		if (state) {
			for (var s in data.config.state) {
				if (data.config.state[s].name == state) {
					data.config.defaults.state  = state;
				}
			}
		}
	}
}

I also reworked the area labeled widget parameters to get the data from the configuration instead of the URL. That area now looks like this:

// widget parameters
data.table_label = gr.getLabel();
data.filter = data.tableData.filter;
data.fields = data.tableData.fields;
data.btnarray = data.tableData.btnarray;
data.refmap = data.tableData.refmap;
data.actarray = data.tableData.actarray;
copyParameters(data, ['p', 'o', 'd', 'relationship_id', 'apply_to', 'apply_to_sys_id']);
data.filterACLs = true;
data.show_keywords  = true;
data.fromJSON = true;
data.headerTitle = (options.use_instance_title == "true") ? options.title : gr.getPlural();
data.enable_filter = options.enable_filter;
data.show_new = options.show_new;
data.show_breadcrumbs = options.show_breadcrumbs;
data.table_name = data.table;
data.dataTableWidget = $sp.getWidget('snh-data-table', data);

No modifications were needed on the client side, so now I just needed to create a new test page, drag the widget onto the page and then use the little pencil icon to configure the widget. I called my new page table_from_json and pulled it up in the Page Designer to drag in this new widget. Using the widget option editor, I entered the name of the Script Include that we have been playing around with lately and left all of the other options that I added blank for this first test.

SNH Data Table from JSON Configuration widget option editor

With that saved, all that was left to do was to go out to the Service Portal and bring up the page.

SNH Data Table from JSON Configuration widget on the new test page

Not bad! I played around with it for a while, trying out different options using the widget option editor in the Page Designer, and everything seems like it all works OK. I’m sure that I’ve hidden some kind of error deep in there somewhere that will come out one day, but so far, I have not stumbled across it in any of my feeble testing. For those of you who like to play along at home, here is an Update Set that I am hoping contains all of the needed parts.

Content Selector Configuration Editor, Part IX

“Plans are only good intentions unless they immediately degenerate into hard work.”
Peter Drucker

It seemed as if we had this topic all wrapped up a while back, but then we had to play around with the Buttons and Icons. That should have been the end of that, but then we went and added Bulk Actions to the hacked up Data Table widget, which has now broken our new Content Selector Configuration Editor. So now we have to add support for the Bulk Action configuration to the editor to put things back together again. Fortunately, this is all very similar stuff, so it is mainly just a matter of making some copies of things that we have already built and then hacking them up just a bit to fit the current need.

To start with, we will need a Bulk Action Editor pop-up very similar to all of the other pop-up editors that we have been creating, and since Bulk Actions only have a name and a label for properties, the State Editor seems like the best choice for something to copy, since it too only has a name and a label for properties. Once we pull that widget up on the widget form, change the name, and then select Insert and Stay from the context menu, we can start working on our new copy. There is literally nothing to change with the HTML:

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="persp"
      snh-label="Name"
      snh-required="true"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

… and only a function name change on the client script:

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

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

As you may recall, there is no server-side code for these guys, so that’s all there is to that. Now we have a Bulk Action Editor. There is a little more work involved in the main Content Selector Configurator widget. On the HTML, we need to define a table for the Bulk Actions:

<div id="label.actarray" class="snh-label" nowrap="true">
  <label for="actarray" class="col-xs-12 col-md-4 col-lg-6 control-label">
    <span id="status.actarray"></span>
    <span title="Bulk Actions" data-original-title="Bulk Actions">${Bulk Actions}</span>
  </label>
</div>
<table class="table table-hover table-condensed">
  <thead>
    <tr>
      <th style="text-align: center;">${Label}</th>
      <th style="text-align: center;">${Name}</th>
      <th style="text-align: center;">${Edit}</th>
      <th style="text-align: center;">${Delete}</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="act in tbl[state.name].actarray" ng-hide="btn.removed">
      <td data-th="${Label}">{{act.label}}</td>
      <td data-th="${Name}">{{act.name}}</td>
      <td data-th="${Edit}" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editAction(act)" alt="Click here to edit this Bulk Action" title="Click here to edit this Bulk Action" style="cursor: pointer;"/></td>
      <td data-th="${Delete}" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteAction(act, tbl[state.name].actarray)" alt="Click here to delete this Bulk Action" title="Click here to delete this Bulk Action" style="cursor: pointer;"/></td>
    </tr>
  </tbody>
</table>
<div style="width: 100%; text-align: right;">
  <action ng-click="editAction('new', tbl[state.name].actarray, tbl);" class="btn btn-primary ng-binding ng-scope" role="action" title="Click here to add a new Bulk Action">Add a new Bulk Action</action>
</div>

On the client side, we need to add a couple more functions to handle the editing and deleting of the actions, which we can basically copy from the same functions we already have for editing and deleting buttons and icons.

$scope.editAction = function(action, actArray) {
	var shared = {page_id: {value: '', displayValue: ''}};
	if (action != 'new') {
		shared.label = action.label;
		shared.name = action.name;
	}
	spModal.open({
		title: 'Bulk Action Editor',
		widget: 'bulk-action-editor',
		shared: shared
	}).then(function() {
		if (action == 'new') {
			action = {};
			actArray.push(action);
		}
		action.label = shared.label || '';
		action.name = shared.name || '';
	});
};

$scope.deleteAction = function(action, actArray) {
	var confirmMsg = '<b>Delete Bulk Action</b>';
	confirmMsg += '<br/>Are you sure you want to delete the ' + action.label + ' Bulk Action?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			var a = -1;
			for (var b=0; b<actArray.length; b++) {
				if (actArray[b].name == action.name) {
					a = b;
				}
			}
			actArray.splice(a, 1);
		}
	});
};

On the server side, we just need to add some code to format the Bulk Action section of the table configurations, which we can copy from the code that formats the Buttons/Icons section, and then delete all of the extra properties that are not needed for Bulk Actions.

script += ",\n				actarray: [";
lastSeparator = '';
for (var a=0; a<tableTable[tableState.name].actarray.length; a++) {
	var thisAction = tableTable[tableState.name].actarray[a];
	script += lastSeparator;
	script += "{\n					name: '";
	script += thisAction.name;
	script += "',\n					label: '";
	script += thisAction.label;
	script += "'\n				}";
	lastSeparator = ",";
}
script += "]";

Now all we need to do is pull up our modified ButtonTestConfig script in the editor and see how we did.

Content Selector Configuration Editor with new Bulk Actions section

So far, so good. Now let’s pull up that new Bulk Action Editor and see how that guy works:

Bulk Action Editor

Not bad. A little bit of testing here and there, just to make sure that we didn’t break anything along the way, and we should be good to go. Now if we can just stop tinkering with things, this Update Set should be the final release and we can move on to other exciting adventures.

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.

Content Selector Configuration Editor, Part VIII

“Tinkering is something we need to know how to do in order to keep something like the space station running. I am a tinkerer by nature.”
Leroy Chiao

I know what you are thinking: Didn’t we wrap this series up last time with the release of the final Update Set? Well, technically you would be correct in that we finished building all of the parts and bundled them all up in an Update Set for those of you who like to play along at home, but … we really did not spend a whole lot of time on the purpose for all of this, so I thought it might be a good time to back up the truck just a tad and demo some of the features of the customized data table widget and the associated content selector widget. Mainly, I want to talk about the buttons and icons and how all of that works, but before we do that, let’s go all the way back and talk about the basic idea behind these customizations of some pretty cool stock products.

I really liked the stock Data Table widget that comes bundled with the Service Portal, but the one thing that annoyed me was that each row was one huge clickable link, which was a departure from the primary UI, where every column could potentially be a link if it contained a Reference field. So, I rewired it. Of course, once you start playing with something then you end up doing all kinds of other crazy things and before you know it, you have this whole subset of trinkets and doodads that only you know anything about. So, on occasion, I feel the need to stop talking about what I am building and how it is constructed, and spend a little time talking about how to use it and what it is good for. Hence, today’s discussion of buttons and icons on the customized Data Table widget.

Before we get started, though, I do need to confess that in all of the excitement around creating a way to edit the JSON configuration object for the Configurable Data Table Widget Content Selector, I completely forgot about one of the options for handling a button click: opening up a new portal page. When we first introduced the idea of having buttons and icons on the rows of the Data Table widget, we made allowances for adding a page_id property to the button definition, and if that property were valued, we would link to that page on a click; otherwise, we would broadcast the click details. We did not include the page_id property in either the Content Selector Configuration Editor widget or the Button/Icon Editor widget, so let’s correct that oversight right now. First, we will need to add that property to the HTML for the table of Buttons/Icons.

<table class="table table-hover table-condensed">
  <thead>
    <tr>
      <th style="text-align: center;">${Label}</th>
      <th style="text-align: center;">${Name}</th>
      <th style="text-align: center;">${Heading}</th>
      <th style="text-align: center;">${Icon}</th>
      <th style="text-align: center;">${Icon Name}</th>
      <th style="text-align: center;">${Color}</th>
      <th style="text-align: center;">${Hint}</th>
      <th style="text-align: center;">${Page}</th>
      <th style="text-align: center;">${Edit}</th>
      <th style="text-align: center;">${Delete}</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="btn in tbl[state.name].btnarray" ng-hide="btn.removed">
      <td data-th="${Label}">{{btn.label}}</td>
      <td data-th="${Name}">{{btn.name}}</td>
      <td data-th="${Heading}">{{btn.heading}}</td>
      <td data-th="${Icon}" style="text-align: center;">
        <a ng-if="btn.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{btn.color || 'default'}}" title="{{btn.hint}}" data-original-title="{{btn.hint}}">
          <span class="icon icon-{{btn.icon}}" aria-hidden="true"></span>
          <span class="sr-only">{{btn.hint}}</span>
         </a>
       </td>
       <td data-th="${Icon Name}">{{btn.icon}}</td>
       <td data-th="${Color}">{{btn.color}}</td>
       <td data-th="${Hint}">{{btn.hint}}</td>
       <td data-th="${Page}">{{btn.page_id}}</td>
       <td data-th="${Edit}" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editButton(btn)" alt="Click here to edit this Button/Icon" title="Click here to edit this Button/Icon" style="cursor: pointer;"/></td>
       <td data-th="${Delete}" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteButton(btn, tbl[state.name].btnarray)" alt="Click here to delete this Button/Icon" title="Click here to delete this Button/Icon" style="cursor: pointer;"/></td>
     </tr>
   </tbody>
 </table>

… and then we will need to pass it back and forth between the main widget and the pop-up Button/Icon Editor widget:

$scope.editButton = function(button, btnArray) {
	var shared = {page_id: {value: '', displayValue: ''}};
	if (button != 'new') {
		shared.label = button.label;
		shared.name = button.name;
		shared.heading = button.heading;
		shared.icon = button.icon;
		shared.color = button.color;
		shared.hint = button.hint;
		shared.page_id = {value: button.page_id, displayValue: button.page_id};
	}
	spModal.open({
		title: 'Button/Icon Editor',
		widget: 'button-icon-editor',
		shared: shared
	}).then(function() {
		if (button == 'new') {
			button = {};
			btnArray.push(button);
		}
		button.label = shared.label || '';
		button.name = shared.name || '';
		button.heading = shared.heading || '';
		button.icon = shared.icon || '';
		button.color = shared.color || '';
		button.hint = shared.hint || '';
		button.page_id = shared.page_id.value || '';
	});
};

… and then, of course, we need to update the Button/Icon Editor itself by dragging in the page selector form field from the Reference Page Editor widget and adding it to the fields on the Button/Icon Editor form.

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="the_name"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.heading"
      snh-name="heading"/>
    <snh-form-field
      snh-model="c.widget.options.shared.icon"
      snh-name="icon"
      snh-type="icon"/>
    <snh-form-field
      snh-model="c.widget.options.shared.color"
      snh-name="color"
      snh-type="select"
      snh-choices='[{"label":"default","value":"default"},{"label":"primary","value":"primary"},{"label":"secondary","value":"secondary"},{"label":"success","value":"success"},{"label":"danger","value":"danger"},{"label":"warning","value":"warning"},{"label":"info","value":"info"},{"label":"light","value":"light"},{"label":"dark","value":"dark"},{"label":"muted","value":"muted"},{"label":"white","value":"white"}]'/>
    <snh-form-field
      snh-model="c.widget.options.shared.hint"
      snh-name="hint"/>
    <snh-form-field
      snh-type="reference"
      snh-model="c.widget.options.shared.page_id"
      snh-name="page_id"
      placeholder="Choose a Portal Page"
      table="'sp_page'"
      display-field="'title'"
      display-fields="'id'"
      value-field="'id'"
      search-fields="'id,title'"/>
    <div id="element.example" class="form-group">
      <div id="label.example" class="snh-label" nowrap="true">
        <label for="example" class="col-xs-12 col-md-4 col-lg-6 control-label">
          <span id="status.example"></span>
          <span title="Example" data-original-title="Example">Example</span>
        </label>
      </div>
      <span class="input-group">
        <a ng-if="!c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">{{c.widget.options.shared.label}}</a>
        <a ng-if="c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">
          <span class="icon icon-{{c.widget.options.shared.icon}}" aria-hidden="true"></span>
          <span class="sr-only">{{c.widget.options.shared.hint}}</span>
        </a>
      </span>
    </div>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

That should take care of that little oversight. Now, let’s get back to showing off the various ways in which you can use buttons and icons on the customized Data Table widget.

To start out, let’s use the tool that we just made to create a brand new configuration for a sample page where we can demonstrate how the buttons and icons work. Let’s click on our new Menu Item to bring up the tool, click on the Create a new Content Selector Configuration button, enter the name of our new Script Include, and then click on the OK button.

Creating a new configuration object using the new tool

When our new empty configuration comes up in the tool, let’s define a single Perspective called Button Test, and a couple of different States, Active and Not Active. We want to keep things simple at this point, mainly so as not to distract from our primary purpose here, which is to show how the buttons and icons work. Once we have defined our Perspective and States, click on the Add new Table button and select the Incident table from the list.

Selecting the Incident table from the pop-up selection list

Select just a couple of fields, say Number and Short Description, and set the filter for the Active State to active=true. Then we can start adding Buttons and Icons using the Add new Button/Icon button.

Adding a new Button/Icon

Those of you paying close attention will notice that the image above was taken before we added the page_id property to the Button/Icon configuration. The newer version has a pick list of portal pages below the Hint and above the Example. You will want to define at least one Button/Icon with a page value. Keep adding different buttons and icons until you have a representative sample of different things and then we can see how it all renders out in actual use.

New configuration with several different buttons and icons

Once you have a few set up for testing, save the new configuration and take a look at the resulting script, which should look something like this:

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

	perspective: [{
		name: 'button_test',
		label: 'Button Test',
		roles: ''
	}],

	state: [{
		name: 'active',
		label: 'Active'
	},{
		name: 'not_active',
		label: 'Not Active'
	}],

	table: {
		button_test: [{
			name: 'incident',
			displayName: 'Incident',
			active: {
				filter: 'active=true',
				fields: 'number,short_description',
				btnarray: [{
					name: 'button1',
					label: 'Button #1',
					heading: '',
					icon: '',
					color: 'info',
					hint: 'This is Button #1',
					page_id: ''
				},{
					name: 'button2',
					label: 'Button #2',
					heading: 'Button #2',
					icon: '',
					color: 'warning',
					hint: 'This is Button #2',
					page_id: ''
				},{
					name: 'button3',
					label: 'Button #3',
					heading: 'B3',
					icon: '',
					color: '',
					hint: 'This is Button #3',
					page_id: 'ticket'
				},{
					name: 'icon1',
					label: 'Icon #1',
					heading: '-',
					icon: 'cross',
					color: 'danger',
					hint: 'Danger Will Robinson!',
					page_id: ''
				},{
					name: 'icon2',
					label: 'Icon #2',
					heading: 'I2',
					icon: 'download',
					color: 'success',
					hint: 'Click here to do something ...',
					page_id: ''
				}],
				refmap: {}
			},
			not_active: {
				filter: 'active=false',
				fields: 'number,short_description',
				btnarray: [],
				refmap: {}
			}
		}]
	},

	type: 'ButtonTestConfig'
});

Now that we have that all ready to rock and roll, we need to configure a new test page so that we can put it to use. From the list of Portal Pages, click on the New button and create a page called button_test and save it so that we can pull it up in the Page Designer.

Setting up the new page in the Page Designer

From the container list, drag over a 3/9 container and drag the Content Selector widget into the narrow portion and the SNH Data Table from URL Configuration widget into the wider portion. You shouldn’t have to edit the Data Table widget, but you will need to click on the pencil icon on the Content Selector widget so that you enter the name of your new configuration script.

Entering the name of the configuration script in the widget options

Once that has been completed, you should be able to pull up your new page in the Service Portal and see how it renders out. You can click on any of your buttons or icons at this point, and if you click on one with a page_id value, that page should come up with the record from that row. If you click on any of the other buttons or icons, through, nothing will happen, because we have not set up anything to react to the button clicks at this point. However, if everything is working as it should be, we are now ready to do just that.

When a button or icon is clicked on the customized Data Table widget, and there is no page_id value defined, all that happens is that the details are broadcast out. Some other widget on the page needs to be listening for that broadcast in order for something to happen. The secret, then, is to have some client-side code somewhere that looks something like this:

$rootScope.$on('button.click', function(e, parms) {
	// do something 
});

For demonstration purposes we can build a simple test widget that will do just that, and in response to a click event, display all of the details of that event in an spModal alert. Let’s call this widget Button Click Handler Example, and give it the following client-side code:

function ButtonClickHandlerExample(spModal, $rootScope) {
	var c = this;

	$rootScope.$on('button.click', function(e, parms) {
		displayClickDetails(parms);		
	});

	function displayClickDetails(parms) {
		var html = '<div>'; 
		html += ' <div class="center"><h3>You clicked on the ' + parms.button.name + ' button</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">Sys ID: &nbsp;</td>\n';
		html += '    <td>' + parms.sys_id + '</td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Button: &nbsp;</td>\n';
		html += '    <td><pre>' + JSON.stringify(parms.button, null, 4) + '</pre></td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Record: &nbsp;</td>\n';
		html += '    <td><pre>' + JSON.stringify(parms.record, null, 4) + '</pre></td>\n';
		html += '   </tr>\n';
		html += '  </tbody>\n';
		html += ' </table>\n';
		html += '</div>';
		spModal.alert(html);
	}
}

Now that we have our widget, we can pop back into the Page Designer and drag the widget anywhere on the screen. It has no displayable content, so it really doesn’t matter where you put it as long as it is present on the page somewhere. Once that’s done, we can pull up our page on the portal again and start clicking around again to see what we get.

Button click results

In addition to the details of the record on that row and the button that was clicked, you also get the name of the table and the sys_id of the record. Any of this data can be used to perform any number of potential actions, all of which are completely outside of the two reusable components, the customized Data Table and the configurable Content Selector. You shouldn’t have to modify either of those widgets to create custom functionality; just configure the Content Selector with an appropriate JSON config object, and then add any custom click handlers to your page for any button actions that you would like to configure. Here is an Update Set that includes the page_id corrections as well as the button test widget so that you can click around on your own and see how everything plays together. There is an even better version here.

Content Selector Configuration Editor, Part V

“It takes considerable knowledge just to realize the extent of your own ignorance.”
Thomas Sowell

At the end of our last installment in this series, we had the HTML for the Tables section, but none of the underlying code to make it all work. Now it’s time to create that code by building all of the client-side functions needed to support all of the ng-click attributes sprinkled throughout the HTML. The first one that you come to in this section is the editButton function that passes the button object as an argument. This should pop open a modal editor just like we did with the Perspectives and the States, so this function could be loosely modeled after those other two. Something like this should do the trick:

$scope.editButton = function(button, btnArray) {
	var shared = {};
	if (button != 'new') {
		shared.label = button.label;
		shared.name = button.name;
		shared.heading = button.heading;
		shared.icon = button.icon;
		shared.color = button.color;
		shared.hint = button.hint;
	}
	spModal.open({
		title: 'Button/Icon Editor',
		widget: 'button-icon-editor',
		shared: shared
	}).then(function() {
		if (button == 'new') {
			button = {};
			btnArray.push(button);
		}
		button.label = shared.label;
		button.name = shared.name;
		button.heading = shared.heading;
		button.icon = shared.icon;
		button.color = shared.color;
		button.hint = shared.hint;
	});
};

We had to add a second argument to the function in this case, but only when creating a new Button/Icon. For an existing object, it is already in place in the object tree, but in the case of a new Button/Icon that we create from a new blank object, we need to know where to put it so that it lives with all of its siblings. Other than that one little wrinkle, the rest is pretty much the same as we have built before. The deleteButton function is also a little different in that we do not have a specified index, so we have to spin through the list to determine the index. Once we have that, though, all we need to do is remove the Button/Icon from the list, as there are no other dependent objects that have to be removed, unlike the Perspectives and the States.

$scope.deleteButton = function(button, btnArray) {
	var confirmMsg = '<b>Delete Button/Icon</b>';
	confirmMsg += '<br/>Are you sure you want to delete the ' + button.label + ' Button/Icon?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			var a = -1;
			for (var b=0; b<btnArray.length; b++) {
				if (btnArray[b].name == button.name) {
					a = b;
				}
			}
			btnArray.splice(a, 1);
		}
	});
};

That takes care of the Buttons/Icons … now we have to do essentially the same thing for the Reference Pages. Once again, we have a few little wrinkles that make things not quite exactly the same. For one thing, the Button/Icon data is stored in an Array, but the Reference Page data is stored in a Map. Also, both of the properties for a Reference Page (Table and Portal Page) have values that come from a ServiceNow database table, so the pop-up editor can use an sn-record-picker for both fields. That means their values will be stored in objects, not strings, so again, not an exact copy of the other functions. Still, it looks pretty close to all of its cousins:

$scope.editRefMap = function(table, refMap) {
	var shared = {table: {}, page: {}};
	if (table != 'new') {
		shared.table.value = table;
		shared.table.displayValue = table;
		shared.page.value = refMap[table];
		shared.page.displayValue = refMap[table];
	}
	spModal.open({
		title: 'Reference Page Editor',
		widget: 'reference-page-editor',
		shared: shared
	}).then(function() {
		if (table != 'new') {
			if (shared.table.value != table) {
				delete refMap[table];
			}
		}
		refMap[shared.table.value] = shared.page.value;
	});
};

… and because of those differences, the deleteRefMap function turns out to be the most simplest of all:

$scope.deleteRefMap = function(table, refMap) {
		var confirmMsg = '<b>Delete Reference Page</b>';
		confirmMsg += '<br/>Are you sure you want to delete the ' + table + ' table reference page mapping?';
		spModal.confirm(confirmMsg).then(function(confirmed) {
			if (confirmed) {
				delete refMap[table];
			}
		});
	};

So now we are done with all of the client-side functions, but our work is still not finished. Both of the edit functions pop open modal editor widgets, so we are going to need to build those. We already have a couple of different models created for the Perspective and State editors, so it won’t be as if we have to start out with a blank canvas. Still, there is a certain amount of work involved, so that sounds like a good place to start out in our next installment.

Content Selector Configuration Editor, Part IV

“Everyone’s time is limited. What matters most is to focus on what matters most.”
Roy Bennett

Now that we have built out all of the easy parts of our new Content Selector Configuration Editor, it’s time to dive into the more complex of the three major sections, the Tables section. In the Tables section, there are configuration properties for every State of every Table in every Perspective, which is a lot of data to put on the page all at once. To help organize this data in a more manageable format, my intent is to leverage the Tabs directives from UI Bootstrap, which are already available on the ServiceNow platform. Immediately under the Tables section header, I would like to see a tab for each Perspective, so that you could focus on one Perspective at a time by selecting that specific tab. Within the tab for each Perspective, I want to have a list of Tables, and then for each Table, another set of nested tabs, one for each State. The contents of the State tab, then, would be all of the properties for that State of that Table in that Perspective, which would include such things as the list of Fields, the query Filter, and any configured Buttons, Icons, or Reference page mappings. At the highest level, it should looks something like this:

Typical Table section layout

It’s still relatively complicated, but the tabs help cut down on the clutter, as you are only looking at one specific Perspective/State at any given time. And it is relatively easy to implement, thanks to the UI Bootstrap components. Here is the basic HTML configuration of the Tables section:

<div>
  <h4 class="text-primary">${Tables}</h4>
</div>
<uib-tabset active="active">
  <uib-tab ng-repeat="persp in c.data.config.perspective" heading="{{persp.label}}">
    <div ng-repeat="tbl in c.data.config.table[persp.name]" style="padding-left: 25px;">
      <h4 style="color: darkblue">{{tbl.displayName}} ({{tbl.name}})</h4>
      <uib-tabset active="active">
        <uib-tab ng-repeat="state in c.data.config.state" heading="{{state.label}}">
          <div style="padding-left: 25px;">

<!-----  all of the specific properties are defined here  ----->

          </div>
        </uib-tab>
      </uib-tabset>
    </div>
  </uib-tab>
</uib-tabset>

Basically it is a DIV within a Tab within a Tabset, which is all tucked into a repeating DIV within a Tab within a Tabset. The innermost DIV will then contain all of the properties for the specific State of the specific Table of the specific Perspective selected. Those properties include the following:

Fields

This is a comma-separated list of the fields to be displayed on a row. To collect this data, I have just provided a single text input element. I would really like to pick these fields from a list, but an sn-record-picker would only select primary fields, and would not give you the option of selecting dot-walked fields from other tables. I could build my own Field Selector widget that supported dot-walking, or maybe there is already one out there that I could borrow, but that seemed like a separate project, so I resisted the temptation to go down that road and just used a single text input element for now. This will work.

Filter

This is just your typical GlideRecord encoded query, and again, I thought about having some kind of query builder UI here that was based on the associated table, but there are plenty of places to go on the platform to build a query, so in the end I decided to take the lazy way out and just define a simple text input element here for now.

Buttons/Icons

In my enhanced version of the Data Table widget, you can define Buttons and Icons to appear on the row with the table data, and the JSON configuration object has a place for an Array of these definitions. For input, we can display these in a simple table, much like we did for the Perspectives and the States, with a pop-up editor to make any changes.

Reference Pages

In the stock Data Table widget, the entire line is a link to the details of the record on that row. In my enhanced version, the first column takes you to that record, but any other columns where the data type is Reference will link you to that Reference record. The standard page for that is the generic form page, but you can override that by specifying your own page for any given table. As I did with the Buttons and Icons, I built a simple table for those, with an associated pop-up editor.

Here, then, is the HTML for a single State tab within a specific table.

<snh-form-field
  snh-label="Fields"
  snh-model="tbl[state.name].fields"
  snh-name="fields"
  snh-required="true"/>
<snh-form-field
  snh-label="Filter"
  snh-model="tbl[state.name].filter"
  snh-name="filter"
  snh-required="true"/>
<div id="label.btnarray" class="snh-label" nowrap="true">
  <label for="btnarray" class="col-xs-12 col-md-4 col-lg-6 control-label">
    <span id="status.btnarray"></span>
    <span title="Buttons/Icons" data-original-title="Buttons/Icons">${Buttons/Icons}</span>
  </label>
</div>
<table class="table table-hover table-condensed">
  <thead>
    <tr>
      <th style="text-align: center;">${Label}</th>
      <th style="text-align: center;">${Name}</th>
      <th style="text-align: center;">${Heading}</th>
      <th style="text-align: center;">${Icon}</th>
      <th style="text-align: center;">${Icon Name}</th>
      <th style="text-align: center;">${Color}</th>
      <th style="text-align: center;">${Hint}</th>
      <th style="text-align: center;">${Edit}</th>
      <th style="text-align: center;">${Delete}</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="btn in tbl[state.name].btnarray" ng-hide="btn.removed">
      <td data-th="${Label}">{{btn.label}}</td>
      <td data-th="${Name}">{{btn.name}}</td>
      <td data-th="${Heading}">{{btn.heading}}</td>
      <td data-th="${Icon}" style="text-align: center;">
        <a ng-if="btn.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{btn.color || 'default'}}" title="{{btn.hint}}" data-original-title="{{btn.hint}}">
          <span class="icon icon-{{btn.icon}}" aria-hidden="true"></span>
          <span class="sr-only">{{btn.hint}}</span>
        </a>
      </td>
      <td data-th="${Icon Name}">{{btn.icon}}</td>
      <td data-th="${Color}">{{btn.color}}</td>
      <td data-th="${Hint}">{{btn.hint}}</td>
      <td data-th="${Edit}" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editButton(btn)" alt="Click here to edit this Button/Icon" title="Click here to edit this Button/Icon" style="cursor: pointer;"/></td>
      <td data-th="${Delete}" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteButton(btn, tbl[state.name].btnarray)" alt="Click here to delete this Button/Icon" title="Click here to delete this Button/Icon" style="cursor: pointer;"/></td>
    </tr>
  </tbody>
</table>
<div style="width: 100%; text-align: right;">
  <button ng-click="editButton('new', tbl[state.name].btnarray, tbl);" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new Button/Icon">Add a new Button/Icon</button>
</div>
<div id="label.refmap" class="snh-label" nowrap="true">
  <label for="refmap" class="col-xs-12 col-md-4 col-lg-6 control-label">
    <span id="status.refmap"></span>
    <span title="Reference Pages" data-original-title="Reference Pages">${Reference Pages}</span>
  </label>
</div>
<table class="table table-hover table-condensed">
  <thead>
    <tr>
      <th style="text-align: center;">${Reference Table}</th>
      <th style="text-align: center;">${Target Page}</th>
      <th style="text-align: center;">${Edit}</th>
      <th style="text-align: center;">${Delete}</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="(key, value) in tbl[state.name].refmap">
      <td data-th="Reference Table">{{key}}</td>
      <td data-th="Target Page">{{value}}</td>
      <td data-th="Edit" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editRefMap(key, tbl[state.name].refmap)" alt="Click here to edit this Reference Page" title="Click here to edit this Reference Page" style="cursor: pointer;"/></td>
      <td data-th="Delete" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteRefMap(key, tbl[state.name].refmap)" alt="Click here to delete this Reference Page" title="Click here to delete this Reference Page" style="cursor: pointer;"/></td>
    </tr>
  </tbody>
</table>
<div style="width: 100%; text-align: right;">
  <button ng-click="editRefMap('new', tbl[state.name].refmap);" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new Reference Page">Add a new Reference Page</button>
</div>

Of course, that’s just the HTML. There are several ng-click functions referenced here that will need to be coded out, along with a number of pop-up editor widgets. And of course, we will need some code to save the whole mess once you get everything configured the way that you would like. Let’s not get too far ahead of ourselves just yet, though. Here we just put one foot in front of the other, taking things on one at a time and focusing on the task at hand. The next task will be some client-side functions to handle all of these newly specified ng-clicks. That sounds like a good place to start, next time out.