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 II

“If you concentrate on small, manageable steps you can cross unimaginable distances.”
Shaun Hick

Before I was sidetracked by my self-inflicted issues with my Configurable Data Table Widget Content Selector, I was just about to dive into the red meat of my new Content Selector Configuration Editor. Now that those issues have been resolved, we can get back to the fun stuff. As those of you who have been following along at home will recall, there are three distinct sections of the JSON object used to configure the content selector: 1) Perspective, 2) State, and 3) Table. Both the Perspective and State sections are relatively simple arrays of objects, but the Table section is much more complex, having properties for every State of every Table in every Perspective. Since I like to start out with the simple things first, my plan is to build out the Perspective section first, work out all of the kinks, and then pretty much clone that working model to create the State section. Once we get through all of that, then we can deal with the more complicated Table section.

As usual, we will start out with the visual components first and try to get things to look halfway decent before we crawl under the hood and wire everything together. Perspectives have only three properties, a Label, a Name, and an optional list of Roles to limit access to the Perspective. We should be able to lay all of this out in a relatively simple table, with one row for each Perspective. Here is what I came up with:

<div>
  <h4 class="text-primary">${Perspectives}</h4>
</div>
<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;">Roles</th>
        <th style="text-align: center;">Edit</th>
        <th style="text-align: center;">Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in c.data.config.perspective">
        <td data-th="Name">{{item.label}}</td>
        <td data-th="Label">{{item.name}}</td>
        <td data-th="Roles">{{item.roles}}</td>
        <td data-th="Edit" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editPerspective($index)" alt="Click here to edit the details of this Perspective" title="Click here to edit the details of this Perspective" style="cursor: pointer;"/></td>
        <td data-th="Delete" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deletePerspective($index)" alt="Click here to permanently delete this Perspective" title="Click here to permanently delete this Perspective" style="cursor: pointer;"/></td>
      </tr>
    </tbody>
  </table>
</div>
<div style="width: 100%; text-align: right;">
  <button ng-click="editPerspective('new')" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new Perspective">Add a new Perspective</button>
</div>

In addition to the columns for the three properties, I also added a column for a couple of action icons, one to edit the Perspective and one to delete the Perspective. I thought about putting input elements directly in the table for editing the values, but I decided that I would prefer to keep everything read-only unless you specifically asked to edit one of the rows. If you do want to edit one of the rows, then I plan on popping up a simple modal dialog where you can make your changes (we’ll get to that a little later).

I also added a button down at the bottom that you can use to add a new Perspective, which should pop up the same modal dialog without any existing values. Here’s what the layout looks like so far:

Perspective section of the configuration object editor

That’s not too bad. I think that it looks good enough for now. Our action icons reference nonexistent client-side functions right now, though, so next we ought to build those out. The Delete process looks like it might be the simplest of the two, so let’s say we start there. For starters, it’s always a good practice to pop up a confirm dialog before actually deleting anything, and I always like to use the spModal confirm option for that as opposed to a simple Javascript confirm. Since deleting the Perspective will also wipe out any Table information defined for that Perspective, we will want to warn them of that as well. Here is what I came up with:

$scope.deletePerspective = function(i) {
	var confirmMsg = '<b>Delete Perspective</b>';
	confirmMsg += '<br/>Deleting the ';
	confirmMsg += c.data.config.perspective[i].label;
	confirmMsg += ' Perspective will also delete all information for every State of every Table in the Perspective.';
	confirmMsg += '<br/>Are you sure you want to delete this Perspective?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			c.data.config.table[c.data.config.perspective[i].name] = null;
			c.data.config.perspective.splice(i, 1);
		}
	});
};

If the operator confirms the delete action, then we first null out all of the Table information for that Perspective, and then we slice out the Perspective from the list. We have to do things in that order. If we removed the Perspective first, then we would lose access to the name, which is needed to null out the Table data. Here is what the confirm dialog looks like on the page:

Perspective Delete Confirmation pop-up

That takes care of the easy one. Now on to the Edit action. For this one, we will use spModal as well, but instead of the confirm method we will be using the open method to launch a small Perspective Editor widget. The open method has an argument called shared that we can use to pass data to and from the widget. Here is the code to launch the widget and collect the updated data when it closes:

$scope.editPerspective = function(i) {
	var shared = {roles:{}};
	if (i != 'new') {
		shared.label = c.data.config.perspective[i].label;
		shared.name = c.data.config.perspective[i].name;
		shared.roles.value = c.data.config.perspective[i].roles;
		shared.roles.displayValue = c.data.config.perspective[i].roles;
	}
	spModal.open({
		title: 'Perspective Editor',
		widget: 'b83b9f342f3320104425fcecf699b6c3',
		shared: shared
	}).then(function() {
		if (i == 'new') {
			c.data.config.table[shared.name] = [];
			i = c.data.config.perspective.length;
			c.data.config.perspective.push({});
		} else {
			if (shared.name != c.data.config.perspective[i].name) {
				c.data.config.table[shared.name] = c.data.config.table[c.data.config.perspective[i].name];
				c.data.config.table[c.data.config.perspective[i].name] = null;
			}
		}
		c.data.config.perspective[i].name = shared.name;
		c.data.config.perspective[i].label = shared.label;
		c.data.config.perspective[i].roles = shared.roles.value;
	});
};

Since this function is intended to be used for both new and existing Perspectives, we have to check to see which one it is in a couple of places. Before we open the widget dialog, we will need to build a new object if this is a new addition, and after the dialog closes, we will have to establish a Table array for the new Perspective as well as establish an index value and an empty object at that index in the Perspective array. Also, if this is an existing Perspective and they have changed the name of the Perspective, then we need to move all of the associated Table information from the old name to the new name and get rid of everything under the old name. Other than that, the new and existing edit processes are pretty much the same.

This takes care of the client side function to launch the widget, but we still need to build the widget. That might get a little involved, and we have already covered quite a bit, so this may be a good place to stop for now. We’ll tackle that Perspective Editor widget first thing next time out, which should wrap up the Perspective section. Maybe we will even have time to clone it all and finish up the State section as well.

Content Selector Configuration Editor

“Nothing in the world can take the place of Persistence. Talent will not; nothing is more common than unsuccessful men with talent. Genius will not; unrewarded genius is almost a proverb. Education will not; the world is full of educated derelicts. Persistence and Determination alone are omnipotent.”
Calvin Coolidge

Some time ago, I built a little Service Portal widget designed to allow a User to select various sets of records to be display in the Data Table widget on a portal page. I called this widget the Configurable Data Table Widget Content Selector because I designed it to be driven by an external JSON configuration object that could be specified as a widget option. Of course, I ended up just hard-coding the first one during my development and had to go back in later and fix it so that you could actually do that, but now that all works — you just have to build a new JSON configuration object if you want to use the widget on a different page for another purpose. That JSON object is a little complicated, though, so it occurred to me that it would be even better if there was some kind of input screen with some validation that would help you put one of those together. I did that not too long ago for the sn-record-picker, which seems to have worked out fairly well, so I was imagining something very similar, but maybe a little more complex.

There are three main sections to the Content Selector: 1) Perspectives, 2) States, and 3) Tables. My configuration wizard, then, would need a simple section where you could set up your Perspectives, another simple section where you could set up your States, and then a more complicated section where you could set up the Tables for every State of every Perspective. That third section sounds a little overwhelming at first glance, but like most complicated issues, if we break it down into its component parts and focus on one thing at time, we should be able to work through it.

The sn-record-picker Helper was just another portal widget, so that seemed like a decent approach to this endeavor as well. I called it the Content Selector Configurator, and placed it alone on a page of the same name for testing. To begin the process, we need to select an existing applicable Script Include, or enter a name if you want to create a brand new one. We can use an sn-record-picker for selecting an existing one, or to make things even easier, an snh-form-field of type reference, which is just a wrapper around the sn-record-picker that includes all of the labels and validation stuff. To limit the selection to just those Script Includes that are relevant to this process, we can filter on the field that contains the actual script looking for the code that extends the base class. The full snh-form-field element looks like this:

<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();"
  snh-required="true"
  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'"/>

… and renders out like this:

Script Include picker

When the selection is made, the snh-change attribute will invoke the scriptSelected() client-side function, so we will need to code that out as well to handle the choice that was made. All of the work necessary to handle the selection will actually occur on the server side, so the client side function just has to kick things over there.

$scope.scriptSelected = function() {
	c.server.update();
};

Over on the server side, things are a little more complicated. We need to use the name of the script to get an instance of the script, and for that, we can use our old friend, the Instantiator. Once we have an instance of the script, we can call the getConfig() function to get a copy of the current configuration object. but before we do that, we have to make sure that we have an input object and we don’t already have a config object. All together, the code looks like this:

if (input) {
	if (!data.config && input.script && input.script.value) {
		data.scriptInclude = input.script.value;
		if (data.scriptInclude.startsWith('global.')) {
			data.scriptInclude = data.scriptInclude.split('.')[1];
		}
		var instantiator = new Instantiator();
		instantiator.setRoot(this);
		var configScript = instantiator.getInstance(data.scriptInclude);
		data.config = configScript.getConfig($sp);
	}
}

Now that we have all of that out of the way, we can work on the actual wizard itself. I wrapped all of the HTML related to selecting a config script in one DIV and then made another, currently empty DIV for the wizard. I used complementary ng-show attributes to hide the wizard until the script was selected, and then hide the selection components once the choice was made. The whole thing now looks like this:

<snh-panel title="'${Content Selector Configuration Editor}'" class="panel-primary">
  <form id="form1" name="form1" ng-submit="save();" novalidate>
    <div class="row" ng-show="!c.data.script.value">
      <div class="col-sm-12">
        <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();"
          snh-required="true"
          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'"/>
      </div>
    </div>
    <div class="row" ng-show="c.data.script.value">
      (the wizard lives here)
    </div>
  </form>
</snh-panel>

I still have to add in the ability to create a new script from scratch, but I think I will deal with that later as I am anxious to jump into the wizard itself. I’ll circle back and toss that in before we wrap things up, but right now I just want to get on to fun stuff. That’s an entirely different process, though, so this seems like a good place to stop for now. We’ll jump straight into the wizard next time out.

Fun with Webhooks, Part X

“Control is for beginners.”
Ane Størmer

I’ve been playing around with our little Incident Webhook subsystem to make sure that everything works, and to make sure that I had finally developed all of the pieces that I had intended to build. For the most part, I’m quite happy with what we have put together during this exercise, but like most end users who finally get their hands on something that they have ordered, now that I have a working model in my hands and have tried to use if for various things, I can envision a number of different enhancements that would make things even better. Still, what we have is pretty nice all on its own, although I did break down and make just a few minor adjustments.

One thing that I had thought about doing, but didn’t, was to skip the confirmation pop-up on the custom Webhook Registry page’s Cancel button when no changes had been made to the form. Going through that a few times was enough to motivate me to put that in there, and I like this version much better. While I was in there, I also built a goBack() function to house the code for returning to the previous page, and then called that function wherever it was appropriate. This didn’t really save that much in the way of code, since the current goBack() logic is only one line itself, but it consolidates the logic in a single place if I ever want to wire in support for something like my Dynamic Breadcrumbs. The entire client side code for the Webhook Registry widget now looks like this:

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

	$scope.cancel = function() {
		if ($scope.form1.$dirty) {
			spModal.confirm('Abandond your changes and return to your Webhooks?').then(function(confirmed) {
				if (confirmed) {
					goBack();
				}
			});
		} else {
			goBack();
		}
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			c.server.update().then(function(response) {
				goBack();
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	function goBack() {
		$location.search('id=my_webhooks');
	}
}

One other thing that I noticed when attempting to integrate with various other targets is that many sites are looking for a property named text as opposed to message. I ended up renaming my message field to text to be more compatible with this convention, but it would really be nice to be able to pick and chose what properties you would like to have in your payload, as well as being able to specify what you wanted them to be named. That’s on my wish list for a future version for sure.

Something that I meant to include in this version, but forgot to do, was to emulate the Test URL UI Action on the Webhook Registry widget so that Service Portal users could have that same capability on that portal page. That was definitely on my plan to include, but I just spaced it out when I was putting that all together. I definitely want to be sure to include that at some point in the near future. I would do it now, but I already built the Update Set and I’m just too lazy to go back and fix it now.

One other thing that is on my wish list for some future version is the ability to set this up for more than just the Incident table. I thought about just switching over to the Task table, which includes Incident as well as quite a few other things derived from Task, but the base Task table does not include the Incident’s Caller or the Request’s Requested for, so there would have to be some special considerations included to cover that. The Task table has Opened by, but that’s not really the same thing when you are dealing with folks calling in and dealing with an Agent entering their information. I thought about adding some additional complexity to cover that, but in the end I just put all of that on my One Day … list and left well enough alone.

Based on what I first set out to do, I think it all came out OK, though. Yes, there are quite a few more things that we could add to make it applicable to a broader domain, and there are a number of things that we could do to make it more flexible, user-friendly, and user-customizable, but it’s a decent start. Certainly good enough to warrant the release of an initial version, which you can download here. Since this is a scoped app, I did not bundle any of the dependencies in the Update Set, so if you want to try this out in your own instance as is, you will need to also grab the latest version of SNH Form Fields and SNH ServiceNow Events, which you can find here. All in all, I am happy with the way that it came out, but I am also looking forward to making it even better one day, after I have spent some time attempting to use it as it is today.

Update: There is a better (improved) version here.

Fun with Webhooks, Part IX

“You have brains in your head. You have feet in your shoes. You can steer yourself any direction you choose.”
Theodor Geisel

We still need to test the My Webhooks portal page that we built last time, but before we do that, I wanted to first build out the page referenced in a couple of links on that page so that we could test everything together. My initial thought for that page was to build out a brand new portal widget for that purpose using our old friends SNH Panel and SNH Form Fields. Before I did that, though, it occurred to me that it might be faster to just use the stock form portal page, passing in the name of the table, and potentially a sys_id for existing records. There were a number of things that I did not like about that idea, but I thought that I could overcome those with some UI Policies and some default values for a couple of table fields. I played around with that a bit and found another thing that I didn’t really like, which was that saving a record left you still on the form page and did not bring you back to the My Webhooks page, which I thought was rather annoying. It seemed as though I might be able to mitigate that by adding my Dynamic Service Portal Breadcrumbs to the top of each page, but then I ran into another problem that I could not work around related to the Document ID field. At that point, I gave up and went back to my original plan, which is where I should have started in the first place.

So, here is the HTML for my new Webhook Registry portal widget:

<snh-panel class="panel panel-primary" title="'${Webhook Registry}'">
  <form id="form1" name="form1" novalidate>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.number"
          snh-name="number"
          snh-label="Number"
          snh-type="text"
          readonly="readonly"/>
        <snh-form-field
          snh-model="c.data.type"
          snh-name="type"
          snh-label="Type"
          snh-type="select"
          snh-required="true"
          snh-choices='[{"label":"Single Item","value":"single"},{"label":"Caller / Requester","value":"requester"},{"label":"Assignment Group","value":"group"},{"label":"Assignee","value":"assignee"}]'/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.owner"
          snh-name="owner"
          snh-label="Owner"
          snh-type="text"
          readonly="readonly"/>
        <snh-form-field
          snh-model="c.data.active"
          snh-name="active"
          snh-label="Active"
          snh-type="checkbox"/>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.url"
          snh-name="url"
          snh-label="URL"
          snh-type="url"
          snh-required="true"/>
      </div>
    </div>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.document_id"
          snh-name="document_id"
          snh-label="Item"
          snh-type="reference"
          snh-required="c.data.type=='single'"
          table="'incident'"
          display-field="'number'"
          search-fields="'number'"
          value-field="'sys_id'"
          ng-show="c.data.type=='single'"/>
        <snh-form-field
          snh-model="c.data.person"
          snh-name="person"
          snh-label="Person"
          snh-type="reference"
          snh-required="c.data.type=='assignee' || c.data.type=='requester'"
          table="'sys_user'"
          default-query="'active=true'"
          display-field="'name'"
          search-fields="'name'"
          value-field="'sys_id'"
          ng-show="c.data.type=='assignee' || c.data.type=='requester'"/>
        <snh-form-field
          snh-model="c.data.group"
          snh-name="group"
          snh-label="Group"
          snh-type="reference"
          snh-required="c.data.type=='group'"
          table="'sys_user_group'"
          default-query="'active=true'"
          display-field="'name'"
          search-fields="'name'"
          value-field="'sys_id'"
          ng-show="c.data.type=='group'"/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.authentication"
          snh-name="authentication"
          snh-label="Authentication"
          snh-type="select"
          snh-required="true"
          snh-choices='[{"label":"None","value":"none"},{"label":"Basic","value":"basic"}]'/>
        <snh-form-field
          snh-model="c.data.username"
          snh-name="username"
          snh-label="Username"
          snh-type="text"
          snh-required="c.data.authentication=='basic'"
          ng-show="c.data.authentication=='basic'"/>
        <snh-form-field
          snh-model="c.data.password"
          snh-name="password"
          snh-label="Password"
          snh-type="password"
          snh-required="c.data.authentication=='basic'"
          ng-show="c.data.authentication=='basic'"/>
      </div>
    </div>
  </form>

  <div style="width: 100%; padding: 5px 50px; text-align: center;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to abandon this update and return to your webhooks">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your input">Save</button>
  </div>
</snh-panel>

There’s nothing too magical there; just a bunch of SNH Form Fields wrapped inside of an SNH Panel. To mirror the UI Policies on the ServiceNow side of things, I used ng-show attributes to hide unneeded fields, and when those fields where required, I used the exact same criteria for the snh-required attribute, which kept it from telling me to complete fields that I couldn’t even see. With just that alone, I could throw the widget onto a page and bring it up, just to see what it looked like.

The new Webhook Registry widget layout

Not too bad, all things considered. Of course, this is just the layout. We still have to put the code underneath this presentation layer. We will definitely need some server side code to read and update the database, but I like to do the easy things first, so let’s start on the client side and throw in the code for the Cancel button. That just takes you right back the My Webhooks page, so that should be pretty simple, although we should bake in a little confirmation pop-up, just to be sure that the operator really does want to abandon their work. We can do that with a plain Javascript confirm, but I like the spModal version much better.

$scope.cancel = function() {
	spModal.confirm('Abandond your changes and return to your Webhooks?').then(function(confirmed) {
		if (confirmed) {
			$location.search('id=my_webhooks');
		}
	});
};

Technically, I should have checked to make sure that something was at least altered before I popped up that confirmation, and if not, just whisked you straight away to the My Webhooks page without asking. I may actually do that at some point, but this works for now. Unlike the Cancel button, the Save button will require some server side code, but we can still code out the client side while we are here and then tackle that next. Here is the code for the Save button.

$scope.save = function() {
	if ($scope.form1.$valid) {
		c.server.update().then(function(response) {
			$location.search('id=my_webhooks');
		});
	} else {
		$scope.form1.$setSubmitted(true);
	}
};

Here we check to make sure that there are no validation errors on the form before invoking the server side code, and then we return to the My Webhooks page once the server process has completed. If there are validation errors, then we set the form status to submitted to reveal all of the field errors to the user. SNH Form Fields hide validation errors until you touch the field or the form has been submitted, so setting the form to submitted here reveals any validation errors present for fields that have not yet been touched.

On the server side, we essentially have two events to handle: 1) widget initialization and 2) handling a Save action. The Save action involves input from the client side, so we know that if there is input present, we are doing a Save; otherwise, we are initializing the widget. At initialization, we need to look for a sys_id parameter in the URL, which tells us that we are updating an existing record. If there isn’t one, then we are adding a new record. For existing records, we need to go get the data and for new records, we need to initialize certain fields. Here is all that code:

data.sysId = $sp.getParameter("sys_id");
if (data.sysId) {
	whrGR.get(data.sysId);
	data.number = whrGR.getDisplayValue('number');
	data.type = whrGR.getValue('type');
	data.url = whrGR.getValue('url');
	data.document_id = {value: whrGR.getValue('document_id'), displayValue: whrGR.getDisplayValue('document_id')};
	data.group = {value: whrGR.getValue('group'), displayValue: whrGR.getDisplayValue('group')};
	data.person = {value: whrGR.getValue('person'), displayValue: whrGR.getDisplayValue('person')};
	data.owner = whrGR.getDisplayValue('owner');
	data.active = whrGR.getValue('active')=='1'?true:false;
	data.authentication =  whrGR.getValue('authentication');
	data.username = whrGR.getValue('username');
	data.password = whrGR.getValue('password');
} else {
	data.owner = gs.getUserDisplayName();
	data.active = true;
	data.document_id = {};
	data.group = {};
	data.person = {};
}

Similarly, when we do a Save, we need to know whether we are doing an update or an insert, which we can again tell by the presence of a sys_id. If we are updating, we need to go out and get the current record, and then in all cases, we need to move the data from the screen to the record and then save it. Here is all of that code:

if (input.sysId) {
	whrGR.get(input.sysId);
}
whrGR.type = input.type;
whrGR.url = input.url;
whrGR.document_id = input.document_id.value;
whrGR.group = input.group.value;
whrGR.person = input.person.value;
whrGR.setValue('active', input.active?'1':'0');
whrGR.authentication = input.authentication;
whrGR.username = input.username;
whrGR.password = input.password;
if (input.sysId) {
	whrGR.update();
} else {
	whrGR.insert();
}

That’s pretty much it for the widget. Just to make sure that it works, we can pull up an existing record and see what shows up on the screen.

Webhook Registry widget with existing record

Once I pulled the record up, I switched the Authentication to Basic and then hit the Save button, just to see if the form validation was working. So far, so good, but there is obviously a lot more testing to do, including the integration with the My Webhooks page. Still, things are looking pretty good at this point.

I’m not quite ready to put out an Update Set at the moment, as there is still quite a bit of testing that I would like to do first. Hopefully, though, I won’t find anything too major and I can drop the whole package next time out.

Fun with Webhooks, Part VIII

“If you are working on something that you really care about, you don’t have to be pushed. The vision pulls you.”
Steve Jobs

When we last parted, there was an open question on the table regarding the focus of this next installment. Implementing Basic Authentication and moving on to the My Webhooks Service Portal widget were two of the options, but no decision was made at the time as to which way we wanted to turn next. Now that we have finally made it here, the good news is that we don’t have to choose. Adding support for Basic Authentication turned out to be so simple that it looks like we are going to have time for both. Check it out:

if (whrGR.getValue('authentication') == 'basic') {
	request.setBasicAuth(whrGR.getValue('user_name'), whrGR.getValue('password'));
}

That’s it. I just added those few lines of code to the postWebhook function of our Script Include and BAM! — now we support Basic Authentication. Pretty sweet!

So now we can devote the remainder of the post to our new My Webhooks widget. As I think I mentioned earlier, this should be a fairly easy clone of the existing My Delegates widget, so the first thing that I did was to make a copy of that guy to have something with which to start. Then I hacked up the HTML to adapt it to our current application.

<table class="table table-hover table-condensed">
  <thead>
    <tr>
      <th style="text-align: center;">ID</th>
      <th style="text-align: center;">Table</th>
      <th style="text-align: center;">Type</th>
      <th style="text-align: center;">Reference</th>
      <th style="text-align: center;">Active</th>
      <th style="text-align: center;">Remove</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="item in c.data.listItems track by item.id | orderBy: 'id'" ng-hide="item.removed">
      <td data-th="ID"><a href="?id=webhook_registry&sys_id={{item.sys_id}}">{{item.id}}</a></td>
      <td data-th="Table">{{item.table}}</td>
      <td data-th="Type">{{item.type}}</td>
      <td data-th="Reference">{{item.reference}}</td>
      <td data-th="Active" style="text-align: center;"><img src="/images/check32.gif" style="width: 16px; height: 16px;" ng-show="item.active"/></td>
      <td data-th="Remove" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteWebhook($index)" alt="Click here to permanently delete this Webhook" title="Click here to permanently delete this Webhook" style="cursor: pointer;"/></td>
    </tr>
  </tbody>
</table>

With that out of the way, we now need to replace the code that gathers up the Delegates with code that will gather up the Webhooks. Seems simple enough …

function fetchList() {
	var list = [];
	var whrGR = new GlideRecord('x_11556_simple_web_webhook_registry');
	whrGR.addQuery('owner', data.userID);
	whrGR.orderBy('number');
	whrGR.query();
	while (whrGR.next()) {
		var thisWebhook = {};
		thisWebhook.sys_id = whrGR.getValue('sys_id');
		thisWebhook.id = whrGR.getDisplayValue('number');
		thisWebhook.table = whrGR.getDisplayValue('table');
		thisWebhook.type = whrGR.getDisplayValue('type');
		thisWebhook.active = whrGR.getValue('active');
		if (thisWebhook.type == 'Single Item') {
			thisWebhook.reference = whrGR.getDisplayValue('document_id');
		} else if (thisWebhook.type == 'Assignment Group') {
			thisWebhook.reference = whrGR.getDisplayValue('group');
		} else {
			thisWebhook.reference = whrGR.getDisplayValue('person');
		}
		list.push(thisWebhook);
	}
	return list;
}

There is still a lot of work to do, but I like to try things every so often before I get too far along, just to make sure that things are going OK, so let’s do that now. This widget could easily go on the existing User Profile page just like the My Delegates widget, but it could also go on a page of its own. Since we are just trying things on for size right now, let’s just create a simple Portal Page and put nothing on it but our new widget. Let’s call it my_webhooks, drop our widget right in the middle of it, and go check it out on the Service Portal.

Initial My Webhooks widget

Well, that’s not too bad. We don’t really need the Save and Cancel buttons in this instance, but we do need a way to create a new Webhook, so maybe we can replace those with a single Add Webhook button. The links don’t go anywhere just yet and the delete icons don’t do anything, but as far as the layout goes, it looks pretty good. I think it’s a good start so far. Let’s swap out the buttons and then we can wrap things up with the client-side code.

The left-over code in the button area from the My Delegates widget looks like this:

<div style="width: 100%; padding: 5px 50px; text-align: center;">
  <button ng-click="saveDelegates()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
   
  <button ng-click="returnToProfile()" class="btn ng-binding ng-scope" role="button" title="Click here to cancel your changes">Cancel</button>
</div>

Let’s replace it with this:

<div style="width: 100%; padding: 5px 50px; text-align: center;">
  <button ng-click="newWebhook()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to create a new Webhook">Add New</button>
</div>

Now we just need to overhaul the code on the client side to handle both the adding and deleting of our webhooks. The code to support the Add New button should be pretty straightforward; we just need to link to the same (currently nonexistent!) page that we link to from the main table, just without any sys_id to indicate that this is a new record request. This should handle that nicely:

$scope.newWebhook = function() {
	$location.search('id=webhook_registry');
};

As for the delete operation, we will have to bounce over to the server side to handle that one, but first we should confirm that was really the intent of the operator with a little confirmation pop-up. We could use the stock Javascript confirm feature here, but I like the look of the spModal version better, so let’s go with that.

$scope.deleteWebhook = function(i) {
	spModal.confirm('Delete Webhook ' + c.data.listItems[i].id + '?').then(function(confirmed) {
		if (confirmed) {
			c.data.toBeRemoved = i;
			$scope.server.update().then(function(response) {
				reloadPage();
			});
		}
	});
};

We’ll need some server side code to handle the actual deletion of the record, but that should be simple enough.

if (input) {
	data.listItems = input.listItems;
	if (input.toBeRemoved) {
		deleteWebhook(input.toBeRemoved);
	}
}

function deleteWebhook(i) {
	var whrGR = new GlideRecord('x_11556_simple_web_webhook_registry');
	if (whrGR.get(data.listItems[i].sys_id)) {
		whrGR.deleteRecord();
	}
}

We still need to test everything, but before we do that, we should go ahead and build the webhook_registry page that we have been pointing at so that we can fully test those links as well. That sounds like a good project for our next installment, so I think we will wrap things up right here for now and then start off next time with our new page followed by some end to end testing.

Fun with Webhooks, Part IV

“Try not to become a man of success, but rather try to become a man of value.”
Albert Einstein

Last time, we came to a fork in the road, not knowing whether it would be better to jump into the My Webhooks Service Portal widget, or to start working out the details of the process that actually POSTs the Webhooks. At this point, I am thinking that the My Webhooks widget will end up being a fairly simple clone of my old My Delegates widget, so that may not be all that interesting. POSTing the Webhooks, on the other hand, does sound like it might be rather challenging, so let’s start there.

When I first considered attempting this, my main question was whether it would be better to handle this operation in a Business Rule or by building something out in the Flow Designer. After doing a little experimenting with each, I later came to realize that the best alternative involved a little bit of both. In a Business Rule, you have access to both the current and previous values of every field in the record; that information is not available in the Flow Designer. In fact, it’s not available in a Business Rule, either, if you attempt to run it async. But if you don’t run it async, then you are holding everything up on the browser while you wait for everything to get posted. That’s not very good, either. What I ultimately decided to do was to start out with a Business Rule running before the update, gather up all of the needed current and previous values, and then pass them to a Subflow, which runs async in the background.

My first attempt was to just pass the current and previous objects straight into my Subflow, but that failed miserably. Apparently, when you pass a GlideRecord into a Subflow, you are just passing a reference, not the entire record, and then when the Subflow starts up, it uses that reference to fetch the data from the database. That’s not the data that I wanted, though. I want the data as it was before the database was updated. I had to take a different route with that part of it, but the basic rule remained the same.

Business Rule to POST webhooks

The rule is attached to the Incident table, runs on both Insert and Update, and is triggered whenever there is a change to any of the following fields: State, Assignment Group, Assigned to, Work notes, or Additional comments.

To get around my GlideRecord issue, I wound up creating two objects, one for current and one for previous, which I populated with data that I pulled out of the original GlideRecords. When I passed those two objects to my Subflow, everything was there so that I could do what I wanted to do. Populating the objects turned out to be quite a bit of code, so rather than put that all in my Business Rule, I created a function in my Script Include called buildSubflowInputs and I passed it current and previous as arguments. That simplified the Business Rule script quite a bit.

(function executeRule(current, previous) {
	var wru = new WebhookRegistryUtils();
	sn_fd.FlowAPI.startSubflow('Webhook_Poster', wru.buildSubflowInputs(current, previous));
})(current, previous);

In the Script Include, things got a lot more complicated. Since I was going to be turning both the current and previous GlideRecords into objects, I thought it would be helpful to create a function that did that, which I could call once for each. That function would pull out the values for the Sys ID, Number, State, Caller, Assignment group, and Assigned to fields and return an object populated with those values.

getIncidentValues: function(incidentGR) {
	var values = {};

	values.sys_id = incidentGR.getValue('sys_id');
	values.number = incidentGR.getDisplayValue('number');
	if (!incidentGR.short_description.nil()) {
		values.short_description = incidentGR.getDisplayValue('short_description');
	}
	if (!incidentGR.state.nil()) {
		values.state = incidentGR.getDisplayValue('state');
	}
	if (!incidentGR.caller_id.nil()) {
		values.caller = incidentGR.getDisplayValue('caller_id');
		values.caller_id = incidentGR.getValue('caller_id');
	}
	if (!incidentGR.assignment_group.nil()) {
		values.assignment_group = incidentGR.getDisplayValue('assignment_group');
		values.assignment_group_id = incidentGR.getValue('assignment_group');
	}
	if (!incidentGR.assigned_to.nil()) {
		values.assigned_to = incidentGR.getDisplayValue('assigned_to');
		values.assigned_to_id = incidentGR.getValue('assigned_to');
	}

	return values;
},

Since my Business Rule could be fired by either an Update or an Insert, I had to allow for the possibility that there was no previous GlideRecord. I could arbitrarily call the above function for current, but I needed to check to make sure that there actually was a previous before making that call. I also wanted to add some additional data to the current object, including the name of the person making the changes. The field sys_updated_by contains the username of that person, so to get the actual name I had to use that in a query of the sys_user table to access that data.

buildSubflowInputs: function(currentGR, previousGR) {
	var inputs = {};

	inputs.current = this.getIncidentValues(currentGR);
	if (currentGR.isNewRecord()) {
		inputs.previous = {};
	} else {
		inputs.previous = this.getIncidentValues(previousGR);
	}
	inputs.current.operator = currentGR.getDisplayValue('sys_updated_by');
	var userGR = new GlideRecord('sys_user');
	if (userGR.get('user_name', inputs.current.operator)) {
		inputs.current.operator = userGR.getDisplayValue('name');
	}

	return inputs;
},

One other thing that I wanted to track was any new comments or work_notes, and that turned out to be the most the most complicated code of all. If you try the normal getValue or getDisplayValue methods on any of these Journal Fields, you end up with all of the comments ever made. I just wanted the last one. I had to do a little searching around to find it, but there is a function called getJournalEntry that you can use on these fields, and if you pass it a 1 as an argument, it will return the most recent entry. However, it is not just the entry; it is also a collection of metadata about the entry, which I did not want either. To get rid of that, I had to find the first line feed and then pick off all of the text that followed that. Putting all of that together, you end up with the following code:

if (!currentGR.comments.nil()) {
	var c = currentGR.comments.getJournalEntry(1);
	inputs.current.comments = c.substring(c.indexOf('\n') + 1);
}
if (!currentGR.work_notes.nil()) {
	var w = currentGR.work_notes.getJournalEntry(1);
	inputs.current.work_notes = w.substring(w.indexOf('\n') + 1);
}

That’s pretty much all that there is to the new Business Rule and the needed additions to the Script Include. Now we need to start tackling the Subflow that will actually take these current and previous objects and do something with them. That’s a bit much to jump into at this point, so we’ll wrap things up here for now and save all of that for our next time out.

Fun with Webhooks

“Good ideas are common – what’s uncommon are people who’ll work hard enough to bring them about.”
Ashleigh Brilliant

There is quite a bit of Webhook stuff in various IntegrationHub spokes, but it all seems to be oriented towards consuming incoming events from different external event publishers. I want to actually be the publisher, and send out information based on some preferences selected by the consumer. That may be hidden somewhere in the Now Platform already, but I can’t seem to find it, so I have decided that I would try to develop a Scoped Application to do just that. This may very well be recreating something that already exists in the platform today, but it sounds like a fun exercise, so I am going to give it the old college try.

As always, I will attempt to start out with the most basic of offerings, and then incrementally expand to add more and better features. My approach is to treat this feature as somewhat analogous to a Watch List, in that you sign up to follow certain events, but instead of sending a notification to a User when the event occurs, the result will be that the information is posted to a specified URL. This can apply to any number of things, but to start off, I am going to focus on some very specific changes to one particular table (Incident), and then later expand from there.

To make this work, there will need to be some kind of Webhook Registry where a consumer would sign up to receive these posts. When registering your webhook, you would enter the URL to which you want the data posted along with the specifics of what type or types of events you would like to have included. I’m thinking about linking them directly to an owner, and having some kind of My Webhooks Portal Page where you could manage your existing registrations and add new ones. When adding a new one, you should be able to enter and test your URL, and for our first iteration, that may be the only choice that you get. Later on, we will want to add the ability to choose what you want to follow, which specific updates should trigger a new post, and even what you would like to have included in the payload. But we will also want to start out as simple as possible, so the initial registry may turn out to be quite barren as far as input fields go.

Once registered, there will need to be some process to actually send out the posts as requested in the registration. This could be a Business Rule on the source table, or maybe something created in the Flow Designer. Either way, the process should scan the registry for any condition matches and then send out a post for each match. Each post and response should be logged in some kind of Webhook Activity Log, and any bad HTTP Response Codes should be reported to Event Management. A robust service would attempt to repost any failures up to a certain limit before giving up completely, but all of that can be delegated to some Alert Management Rule at some later time. Again, we will want to start out simple, so our initial focus will just be on making that initial post attempt. Everything else can be pushed off until later on in the process.

Those would seem to be the two major functions: registering the webhook and sending out the posts. We may want some other things at some point, such as the ability to review the logs or to manually repost or to clone an existing registration, but for now, just those two things should get the ball rolling. We may also want to set up a sample receiver for testing purposes, but in practice, the receivers would be other products and outside the scope of this development exercise. There is actually an existing service out on the Internet called Webhook.site that might turn out to be just what I need in order to do a little testing. We should check that out when we get to that point.

For our parts list, then, I can see the need for the following artifacts:

  • A table to hold the webhook registrations,
  • A my_webhooks portal widget to list all webhooks owned by the user,
  • A webhook portal widget for editing a single webhook registration,
  • A Business Rule or Flow to send out the posts,
  • A log table to record the posts and response, and possibly
  • A Script Include to contain some common functions.

Of course, before we create any of that, we will have to create the Scoped Application itself, so that should be where we start next time when we initiate the actual construction phase of this effort.

@mentions in the Ticket Conversations Widget, Revisited

“Continuous improvement is better than delayed perfection.”
Mark Twain

When I hacked up the Ticket Conversations Widget to add support for @mentions, I knew that a number of people had already asked ServiceNow to provide the same capability out of the box. I also knew, though, that these things take time, and not wanting to wait, I just charged ahead as I am often prone to do. However, I was happy to hear recently that the wait may not be all that much longer:

Hi,

The state of idea Service Portal – @Mention Support has been marked as Available

Work for idea is complete and planned to be released in the next Family version

Log in to Idea Portal to view the idea.

Note: This is a system-generated email, do not reply to this email id.
If you would like to stop receiving these emails you can change your preferences here.

Sincerely,
Community Team  

I am assuming that the “next Family version” is a reference to Quebec, although I have nothing on which to base that interpretation. It’s either that or Rome, so one way or another, it appears to be on the way. If and when it does arrive, I will gladly toss my version into the trash and use the real deal instead. I don’t mind building out things that I want that the product currently doesn’t provide, but as soon the product does provide it, my theory is that you fully embrace the out-of-the-box version and throw that steaming pile of home-grown customized nonsense right out the window. I actually look forward to that day.

But then again, that day is not today! Not yet, anyway.

Configurable Data Table Widget Content Selector, Revisited

“Learning from mistakes and constantly improving products is a key in all successful companies.”
Bill Gates

When I first conceived of my Configurable Data Table Widget Content Selector, my main focus was on creating a process that would read the JSON configuration file and turn those configuration rules into a functioning widget in accordance with the specifications. That was an interesting challenge that I had a quite a bit of fun with, but I started out by hard-coding the configuration object at the beginning of the widget and I never went back and set things up so that you could reuse the widget with a different configuration. Obviously, that’s not very friendly; now I need to go back in and set things right. The configuration object that you want to use should be an external parameter that gets passed into the widget via some external source such as a URL parameter or widget option. Let’s see if we can’t fix that right now.

I think that the first thing that I want to do is to create a base class for the configuration object script. That will do two things: 1) provide a common foundation of code for all of the configuration objects that you would like to build, and 2) provide a way to identify all of the qualifying scripts as all scripts that extend this particular base class. We’ll call it the ContentSelectorConfig:

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

	initialize: function() {
	},

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

	type: 'ContentSelectorConfig'
};

With our base class established, I can now hack up my earlier configuration object and make it an extension of this new base class:

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

	perspective: [...],

	state: [...],

	table: {...},

	type: 'MyTaskConfig'
});

For now, I am just going to reuse the list of perspectives, states, and tables that I was using before and focus on the mechanics of making this configuration an external parameter rather than a hard-coded reference. Rather than make the changes to my original My Data widget, though, I decided to clone the widget, give it a new name, and leave the original widget as is. I called my new widget Content Selector, and it started out life as an exact copy of the My Data widget before I started to hack it up.

The first thing that I did was to add a new option to the Option Schema so that we could pass in the name of the ContentSelectorConfig that we want to use. We already had an existing option to display the widget content in a single row rather than a stacked block, so this was just a matter of adding a second option to the existing array of options.

[{
  "hint":"Mandatory configuration script that is an extension of ContentSelectorConfig",
  "name":"configuration_script",
  "section":"Behavior",
  "label":"Configuration Script",
  "type":"string"
},{
  "hint":"If selected, will display the widget content in a single row rather than a stacked block",
  "name":"display_inline",
  "default_value":"false",
  "section":"Presentation",
  "label":"Display Inline",
  "type":"boolean"
}]

Now that our new option has been defined, it’s time to rewrite the code that pulled in the hard-coded configuration object. Here is the original version of the first few lines of code in the server side script:

var mdc = new MyDataConfig();
data.config = mdc.getConfig($sp);
data.config.authorizedPerspective = getAuthorizedPerspectives();
establsihDefaults();
data.user = data.user || {sys_id: gs.getUserID(), name: gs.getUserName()};
data.inline = false;
if (options && options.display_inline == 'true') {
	data.inline = true;
}

We are still going to want to establish the data.config object, but we are going to want to do it using an instance of the class named in our new Option. Last year, when I was working on my Static Monthly Calendar, I needed to turn a class name into an object of that class, and I built a little tool for that, which I called the Instantiator. We can use that same tool here to turn the name of our configuration script into an instance of that class that we can use to pull in the configuration. Here is the restructured code to start out our updated widget:

data.config = {};
data.inline = false;
data.user = data.user || {sys_id: gs.getUserID(), name: gs.getUserName()};
if (options) {
	if (options.configuration_script) {
		var instantiator = new Instantiator();
		instantiator.setRoot(this);
		var configurator = instantiator.getInstance(options.configuration_script);
		data.config = configurator.getConfig($sp);
		data.config.authorizedPerspective = getAuthorizedPerspectives();
		establsihDefaults();
	}
	if (options.display_inline == 'true') {
		data.inline = true;
	}
}

Now we just need to throw it on a page with a Data Table widget, configure the options, and give it a spin. After dragging all of the widgets onto the page in the Page Designer, clicking on the pencil icon in the upper right-hand corner of our update widget will bring up the Options dialog where we can specify our configuration script.

Widget Options dialog

Once the widget Options have been specified, all we need to do is to pull up the page and see if things are still functioning as they should, which appears to be the case.

Testing the completed modifications

Well, that’s about all there is to that. Basically, it does exactly what it did before, but you can now specify the configuration script using the Widget Options instead of having it hard-coded in the script as it was in the original version. Here’s an Update Set with the modifications if you’d like to play around with it on your own.