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.

Configurable Data Table Widget Content Selector, Corrected

“No great thing is created suddenly.”
Epictetus

While playing around with my new Content Selector Configuration Editor, I ran into a few errors in my Configurable Data Table Widget Content Selector when working in the Portal Page Designer. The problem that I ran into was that errors in widget prevented the widget from appearing in the container, which then prevented you from accessing the widget controls that let you edit the widget options or delete the widget. I had run into something similar before with my Dynamic Service Portal Breadcrumbs, so I pretty much knew what was going on — I just needed to hunt down the specific error. In this particular case, it turned out to me more than one error, and fixing the first one still did not solve the problem completely.

The problem occurs when first dragging the widget onto a page. Before you have an opportunity to edit the widget options and specify a configuration script,the widget tries to run without a configuration script and then it crashes because it has no configuration script. Here is the offending code:

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

The problem is that second line that wants to grab the default table value from the defaults object in the configuration. Since we haven’t had a chance to specify a configuration script just yet, there is no defaults object in the configuration, and attempting to access the table property of that nonexistent object will earn you a NullPointerException. That’s not good! Before checking for the table property, we need to first check to see if the defaults object exists. This modification should do the trick:

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

That keeps the widget from crashing, but there is still no content displayed on the screen in the Page Designer, so I decided to add a little something to the top of the HTML that would only show if there was no configuration script specified. That code looks like this:

<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>

Now when you drag a brand new copy of the widget onto the canvas in the Page Designer, you get this:

Content Selector widget with no configuration script specified

That takes care of an empty configuration script name, but what if you enter a name for script that isn’t a valid Script Include? Well, that crashes the widget code as well, so we will need to fix that, too. This time, the issue is on the server side, where we were assuming that we would always get an instance of whatever script was specified. As you can see from the following code snippet, we don’t even bother to check to see if something was returned from the Instantiator:

var configurator = instantiator.getInstance(options.configuration_script);
data.config = configurator.getConfig($sp);
data.config.authorizedPerspective = getAuthorizedPerspectives();
establsihDefaults();

That’s another easy fix, though. We just need to check to make sure that it is there before we attempt to use it.

var configurator = instantiator.getInstance(options.configuration_script);
if (configurator != null) {
	data.config = configurator.getConfig($sp);
	data.config.authorizedPerspective = getAuthorizedPerspectives();
	establishDefaults();
}

Here is another instance where it would be good to let the developer know that something is amiss, so I added yet anothe DIV to the top of the HTML:

<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>

Here’s how that looks in action:

Error message when a invalid Script Include is specified

That should resolve all of the errors that I have discovered so far. Here is the corrected Update Set, which should replace all of the broken parts in the last one and get things working again.

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.

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.

Parts is Parts

“Parts is parts.”
Wendy’s TV commercial

I love making parts. One of the reasons that ServiceNow is such a powerful platform is that it is built on reusable parts, and the platform encourages the development and use of reusable parts. I have known a number of developers who are extraordinary coders that can lay down reliable code at an amazing rate of speed, but no one can code faster than they can pull an existing part down from a shelf. When you build a part in such a way that you can reuse it again in another context, you not only solve your current problem, but you also create the solution for problems that you haven’t even encountered yet. That doesn’t just improve productivity, it also increases reliability. Parts on the shelf are on the shelf because you have used them somewhere before, and if you’ve used them before, then you’ve tested them before, which means that you’ve already gone through the exercise of shaking out all of those initial bugs. Faster and better — it’s a win/win.

When I started my Highcharts experiment, my goal was to create a reusable component for displaying any Highcharts graph. I was able to do that with my Generic Chart widget, but in the process, I also ended up showcasing a number of the other little parts and pieces that have been developing on the Now Platform. For example, when I wanted to add the ability to click on the chart and drill down to the underlying data, I ended up linking to my enhanced Data Table widget. I actually built that to support my configurable content selector, but once created (and tested) and placed on the shelf, it was there to later pull down, dust off, and utilize for other, previously unforeseen purposes.

To make the various selections for the four different parameters used in the workload chart, I ended up using the angular provider that I put together to produce form fields on the Service Portal. I didn’t create that to support the workload chart, but once it was created, there is was on the shelf to pull down and put to use.

I built my dynamic Server Portal breadcrumbs because I really didn’t like the way that the out-of-the-box breadcrumbs widget required you to pre-define the trail of pages displayed by the widget. That philosophy assumes that each of your pages can only be reached through a single path. For someone who likes to build and leverage reusable parts, this just seemed a little too restrictive to me. This was yet another “part” that I had build with my content selector in mind, but which was equally useful once I started linking out from my workload chart. In fact, it was while working with my workload chart that I discovered a flaw in my breadcrumbs widget. That’s another nice thing about working with reusable parts: when you fix a problem, you don’t just fix it for your purposes; you fix it for everyone else who is using it for whatever other purpose. That’s the difference between cloning and reusing. If you clone a part and find a flaw, you fix your copy, but the original and any other clones are unaffected. If you reuse a part and fix a flaw, you fix it for everyone.

That’s why I love making parts.

Fun with Highcharts, Part VII

“Time, which alone makes the reputation of men, ends by making their defects respectable.”
Voltaire

Once again, things were not quite as bad as I had originally envisioned. I definitely disliked returning to my workload chart page and having it revert back to all of the original, default settings. This was not good. But how best to fix it? I didn’t have that problem with my Data Table Content Selector, which also has multiple options, but that was built using a little bit different approach. Although there are two independent widgets on the page, they don’t really communicate with one another. The selector on the current page communicates with the Data Table widget on the next page, passing all of the information via URL parameters when it builds the URL for the next page. Reconfiguring the workload chart widget and underlying generic chart widget to adopt this philosophy seemed like a lot more work that I really wanted take on right at the moment.

Instead, I decided to leverage the same hack that I used in my dynamic breadcrumbs widget, leveraging the User Preferences feature to preserve the selections and then picking them back up each time the page loads. Here is the additional code I added to save the user’s selections in a User Preference that I called workload_selections:

var selections = {};
selections.group = data.group;
selections.type = data.type;
selections.frequency = data.frequency;
selections.ending = data.ending;
gs.getUser().setPreference('workload_selections', JSON.stringify(selections));

User Preferences are strings, so you have to convert the object to a string before you save it, and convert it back to an object when you fetch it back. Here is the code that I used to retrieve it once the page loads again:

selections = gs.getPreference('workload_selections');
if (selections) {
	selections = JSON.parse(selections);
	if (selections.group) {
		data.group = selections.group;
		data.type = getDefaultType();
		data.type = selections.type;
		data.frequency = selections.frequency;
		data.ending = selections.ending;
	}
}

You might have noticed that the value for the type selection was set twice, once to the default value and then again to the value saved in the User Preference. That’s because the getDefaultType function is actually a dual purpose function; not only does it return the default type value, but before it does so, it sets up the type options that are appropriate for the selected group (different groups are associated with different types of tasks). Since the group value preserved may be different than the default value, the appropriate list of type choices needs to be generated for that group before the preserved type value is established. Other than that, it’s pretty straightforward stuff.

That resolved the last known defect that I have been able to find so far, so it’s finally time to release another Update Set. If you happen to pull it down and try to use it, let me know if you find any others … thanks!

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

Configurable Data Table Widget Content Selector, Part IV

“It has long been an axiom of mine that the little things are infinitely the most important.”
Sir Arthur Conan Doyle

When we last left this particular widget, I was experiencing problems with the companion button-handling widget executing more than one time per click. While my little conditional work-arounds seem to have hidden the unwanted results of that unfortunate behavior, I am still no closer to understanding how or why that is happening. In my simple mind, one click on the button should result in one execution of the activated code. Given that I don’t really know what I am doing, I may never understand why that isn’t the case, but it annoys my sense of The Way Things Ought To Be. Still, it’s probably long past time to simply move on.

Before I do, though, there is one more little enhancement that I wanted to squeeze in. Whenever I lay out a page where the content selector is on the top, instead of on the left- or right-hand side of the actual Data Table, it takes up too much vertical space and leaves a lot of unused screen real estate on either side. To change that, I added a new option to the widget called display_inline that changes the way the elements of the widget are laid out. Below is a sample use case where display_inline has been set to true.

Content selector widget in “in-line” mode

The magic to pull that off was just the addition of conditional class attributes on each of the three primary DIV elements. There was already a class attribute on each, so I could have just thrown some logic in there, but in the end I decided to leverage the AngularJS ng-class attribute instead. By adding the Bootstrap class col-sm-4 to the DIV whenever data.inline is true, the DIVs end up side by side instead of their normal stacked configuration.

    <div class="panel panel-heading" ng-class="{'col-sm-4': c.data.inline}" ng-show="c.data.config.authorizedPerspective.length > 1">
      <div align='center' class="u-space-bottom u-align-center">
        <b>Perspective</b>
        <br/>
        <span ng-repeat="p in c.data.config.authorizedPerspective" style="margin: 5px;">
          <input style="display: inline;" type="radio" name="perspective" id="{{p.name}}Perspective" value="{{p.name}}" ng-model="c.data.perspective" ng-click="selectPerspective(p.name)">
          <label style="display: inline;" for="{{p.name}}Perspective">{{p.label}}</label>
        </span>
      </div>
    </div>

    <div class="panel panel-heading" ng-class="{'col-sm-4': c.data.inline}">
      <div align='center' class="u-space-bottom u-align-center">
        <button ng-repeat="s in c.data.config.state" ng-click="selectState(s.name)" role="button" ng-class="(c.data.state==s.name) ? 'btn btn-primary btn-pressed btn-md' : 'btn btn-default btn-sm'" style="margin: 2px;">{{s.label}}</button>
      </div>
    </div>
  
    <div class="panel" ng-class="{'col-sm-4': c.data.inline, 'panel-heading': c.data.inline, 'panel-default': !c.data.inline}">
      <ul class="list-group">
        <li class="list-group-item" ng-repeat="t in c.data.list" ng-class="(t.name==c.data.table)?'highlight':''" ng-click="selectTable(t.name)">{{t.label}}
          <span class="badge">{{t.value}}</span>
        </li>
      </ul>
    </div>

To process the option, I initially default the variable value to false, and the reset it to true if the option was set to the string ‘true’.

data.inline = false;
if (options && options.display_inline == 'true') {
	data.inline = true;
}

Of course, I had to define the Display Inline option as well, but that was just a matter of updating the widget’s Option schema will a little JSON object:

[{"hint":"If selected, will display the widget content in a single row rather than a stacked block",
"name":"display_inline",
"default_value":"false",
"section":"Behavior",
"label":"Display Inline",
"type":"boolean"}]

That’s about it for allowing this to stretch out across the page rather than be stacked up in the corner. Hopefully, this will be the last you see of this unless I happen to figure out my other issue. For those of you who are interested in seeing all of the parts and pieces in detail, I have assembled everything into yet another Update Set, which you can grab from here.

Configurable Data Table Widget Content Selector, Part III

“Everyone thinks of changing the world, but no one thinks of changing himself.”
Leo Tolstoy

After all of that work on customizing the Data Table widget(s), I realized that my Data Table Content Selector widget didn’t support all of the new features. If I wanted to have buttons or icons or customized reference links, I needed to tweak the code a little bit to provide that capability. Primarily, I needed to expand the schema for my configuration object to include options for buttons and reference pages for every table configuration. That would make the typical state value for a given table look something like this:

open: {
    filter: 'caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Eactive%3Dtrue',
    fields: 'number,opened_by,opened_at,short_description',
    btnarray: [],
    refmap: {sys_user: 'user_profile'}
},

To maintain backwards compatibility, both of the new options, btnarray and refmap, would need to be optional. Since the content selector widget relies on the URL-based version of the Data Table widget, implementing the new features was simply a matter of including them, if present, in the new URL at every page refresh:

function refreshPage(table, perspective, state) {
	var tableInfo = getTableInfo(table, perspective);
	var s = {};
	s.id = $location.search().id;
	s.table = tableInfo.name;
	s.filter = tableInfo[state].filter;
	s.fields = tableInfo[state].fields;
	s.buttons = '';
	if (tableInfo[state].btnarray && Array.isArray(tableInfo[state].btnarray) && tableInfo[state].btnarray.length > 0) {
		s.buttons = JSON.stringify(tableInfo[state].btnarray);
	}
	s.refpage = '';
	if (tableInfo[state].refmap) {
		s.refpage = JSON.stringify(tableInfo[state].refmap);
	}
	s.px = perspective;
	s.sx = state;
	var newURL = $location.search(s);
	spAriaFocusManager.navigateToLink(newURL.url());
}

That was basically all there was to it. Now I just need to create a new configuration object that takes advantage of these new features. My original example configuration contained two perspectives, Requester and Fulfiller. To show off the new buttons feature, I decided to add a third perspective, Approver, and then include three separate icons, one to Approve, one to Approve with comments, and another to Reject. The button configuration that I created to support this turned out like this:

btnarray: [
	{
		name: 'approve',
		label: 'Approve',
		heading: '-',
		icon: 'workflow-approved',
		color: 'success',
		hint: 'Click here to approve'
	},{
		name: 'approvecmt',
		label: 'Approve w/Comments',
		heading: '-',
		icon: 'comment-hollow',
		color: 'success',
		hint: 'Click here to approve with comments'
	},{
		name: 'reject',
		label: 'Reject',
		heading: '-',
		icon: 'workflow-rejected',
		color: 'danger',
		hint: 'Click here to reject'
	}
]

After entering some additional modifications to the configuration to add the new perspective, the resulting page ended up looking like this:

Approver perspective with Action Icons

That took care of the look and feel, but to make the buttons actually work, I needed to create another button handling widget to process the button clicks on the three icons that I had configured. For that, I just grabbed the example that I had created earlier and cloned it to create a new Approval Click Handler widget. Here is the client script:

function(spModal, $rootScope) {
	var c = this;
	$rootScope.$on('button.click', function(e, parms) {
		if (!c.data.inProgress) {
			c.data.inProgress = true;
			c.data.sys_id = parms.record.sys_id;
			c.data.action = parms.button.name;
			c.data.comments = '';
			if (c.data.action == 'reject' || c.data.action == 'approvecmt') {
				getComments(c.data.action);
			} else if (c.data.action == 'approve') {
				processDecision();
			}
			c.data.inProgress = false;
		}
	});

	function getComments(state) {
		var msg = 'Approval comments:';
		if (state == 'reject') {
			msg = 'Please enter the reason for rejection:';
		}
		spModal.prompt(msg, '').then(function(comments) {
			c.data.comments = comments;
			processDecision();
		});
	}

	function processDecision() {
		c.server.update().then(function(response) {
			window.location.reload(true);
		});
	}
}

… and here is the server side script:

(function() {
	if (input) {
		var current = new GlideRecord('sysapproval_approver');
		current.get(input.sys_id);
		if (current.state == 'requested') {
			current.state = 'approved';
			if (input.action == 'reject') {
				current.state = 'rejected';
			}
			var comments = 'Approval response from ' + gs.getUserDisplayName() + ':';
			comments += '\n\nDecision: ' + current.getDisplayValue('state');
			if (input.comments) {
				comments += '\nReason: ' + input.comments;
			}
			current.comments = comments;
			current.update();
		}
	}
})();

When I first pulled this up and tested the various buttons, a single click appeared to launch multiple iterations of the code. After entering comments in the modal pup-up box, another comment entry box would pop-up as if I had clicked on the icon a second time. Looking at the resulting records, the entered comments would often appear multiple times. On one example, it was entered 10 times! I never did figure out why that was happening, but I added conditionals to both the client side and server side scripts in an effort to put a stop to that behavior. That seems to have stopped it, but that still doesn’t explain to me why that is happening.

Looks like I have a little more testing to do before I put together a final Update Set

Customizing the Data Table Widget, Part VII

“Never give up on something that you can’t go a day without thinking about.”
Winston Churchill

Those of us who develop software for a living always like to blame the customer for the inevitable scope creep that works its way into an assignment or project. The real truth of the matter, though, is that a lot of that comes right from the developers themselves. Often, just when you think you are about to wrap something up and call it complete, you get that nagging you-know-it-would-be-even-better-if-we-added-this feeling that just won’t go away.

It was my intention to make my previous installment on the Custom Data Table widget my last and final offering on the subject. I had this idea to create a custom Service Portal breadcrumbs widget that didn’t require you to specify the entire page hierarchy on every page where it was included, and I was ready to dive into that little adventure. But there were still a couple of things pulling at me on the Data Table widget(s), and I just felt compelled to wrap those up before I moved on. For one, I never really implemented the buttons and icons as URL parameters in the version of the wrapper widget that was based on the URL. On top of that, I never really liked reusing the same page for all reference links; I wanted to create the capability to have different pages for different references. None of that was really super critical, but I couldn’t really call it complete until I took care of that, so here we are.

For the reference links, I decided to create a simple JSON object that mapped reference tables to portal pages. Primarily, I wanted to send all user references to the user_profile page, but I also wanted to create possibility of sending any reference to any page. So I added yet another configuration option, much like the other two that are already there:

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

This creates yet another text input on the widget configuration page just under the one that we created for the button specifications:

Reference Page specification input field for the JSON object

The JSON object itself is just a simple mapping of table name to portal page name (id). To support my intent to bring up all users via the User Profile page, I used the following JSON object:

{
    "sys_user": "user_profile"
}

To add more options, you just add more properties to the object using the table name as the property name and the associated page name/id as the property value. To make all of that work, I had to pull in the JSON string and then parse it out to create the actual object to be used.

if (data.refpage) {
	try {
		var refinfo = JSON.parse(data.refpage);
		if (typeof refinfo == 'object') {
			data.refmap = refinfo;
		} else {
			gs.error('Invalid reference page option in SNH Data Table widget: ' + data.refpage);
			data.refmap = {};
		}
	} catch (e) {
		gs.error('Unparsable reference page option in SNH Data Table widget: ' + data.refpage);
		data.refmap = {};
	}
} else {
	data.refmap = {};
}

Once you have all of that tucked away for future reference, whenever a reference link is clicked, we simply refer to the map to see if there is a specific page associated to the table specified in the reference link. If there is, we simply pass that along with the rest of the parameters when we broadcast the reference click.

if (c.data.refmap[table]) {
	parms.page_id = c.data.refmap[table];
}

For those listening for the reference click, all we needed to do was to check for the presence of a page_id in the parms before we check for a page_id in the options, which we were already doing before we settled on the default page of ‘form’.

var p = parms.page_id || $scope.data.page_id || 'form';

That’s about it for supporting table-specific reference link pages. The other thing that I wanted to do was to make sure that my Data Table from URL Definition widget also supported buttons and icons as well as the new reference link page specifications. That turned out to be a simple matter of just adding those two extra options to the existing copyParameters function call to have those values pulled in from the URL and added to the data passed to the core Data Table widget.

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

With that in place, you can add something like &refpage={“sys_user”:”user_profile”} to the URL for the page and have that picked up by the Data Table from URL Definition widget and processed just as if it were specified in the widget options. I’ve wrapped all of that up into yet another Update Set for those of you who are into that sort of thing. Hopefully, this will bring this little adventure to a close now, and I can move on to other things.

Customizing the Data Table Widget, Part VI

“A person who never made a mistake never tried anything new.”
Albert Einstein

Well, it turns out it didn’t take all that long to figure out why I was losing the buttons and icons on my customized Data Table widget whenever I sorted the data. It was definitely my fault, which was easily predictable based on the The First Rule of Programming. I actually had one of those this is not right feelings at the time that I did it, but I ignored that and did it, anyway. When I was parsing the JSON string for the button configuration to obtain the actual JSON array of button specs, I reused the same variable to hold the array as the original variable that held the string. Here is the offending code:

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

The problem with doing that, which is just a bad practice in general and I’ve been around long enough to know better, is that we pass through this same code again and again whenever the table is sorted. Reusing the same variable means that when you come through this logic the next time around, things are not the same as they were the first time through. That’s not good. I tried several failed attempts at detecting and avoiding the problem, but I finally broke down and just created a new variable to hold the button spec array and left the original JSON string intact. That solved the problem. Here is what it looks like now:

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

Of course, once I changed the name of the variable, then I had to hunt down and modify every reference to that variable, which was a task in itself. But I’ve done the work now, and retested everything, so here’s another Update Set with the corrected code. Lesson learned (yeah, right!).