Aggregate List Columns, Part X

“You’re always trying to get better. You’re always tinkering. You’re always learning new things.”
Kyle Korver

The previous installment in this series was supposed to be the last, as I wanted to get back to the Collaboration Store project that was just left hanging after the call for outside testing assistance. But then I decided to post the Update Set out on Share, and now that it is out there, it really disturbs my sense of The Way Things Ought To Be that you cannot click on an aggregate column and pull up a list of the records represented in that number. It doesn’t seem like it would take much to add that feature, and I am not sure why I never included it in the first place, but now that it is out there for all of the world to see, I feel as if I need to correct that omission. So here we go.

There are a couple of different ways to show a list of the records. One would be to simply link to a new page passing the sys_id of the row and the name of the table. Another would be some kind of modal pop-up containing a simple list or data table containing the records. To provide for all of these possibilities, we could add yet one more property to the configuration object for an aggregate column that we could call action. The value of the property could either be a URL query string for the new page, or the word broadcast, which would send out a broadcast message on click so that a companion widget could handle the modal dialog independently, outside the scope of the data table widget. We already have examples of this with the buttons and icons, and this would embrace that same strategy.

The easiest place to start with such a strategy would be with the Content Selector Configuration Editor, adding the new property to the table of aggregate column specifications as well as the pop-up aggregate column specification editor. Let’s start with the main widget and add the following to the list of column headings for that section:

<th style="text-align: center;">${Action}</th>

And then in the repeating rows of that table, let’s insert this line:

<td data-th="${Action}">{{agg.action}}</td>

On the server side, in the Save() function that rebuilds the script, let’s add these lines in the aggregate column specification section:

 script += "',\n                 action: '";
 script += thisAggregate.action;

Finally, on the client side, let’s add the following line to the new record section of the editAggregate() function:

shared.action = aggregate.action;

… and this line to the section that saves the edits in that same function:

aggregate.action = shared.action || '';

That takes care of the main widget, but we also need to add a new input field to the pop-up editor before this will actually work, so we need to add these lines to the HTML of the Aggregate Column Editor widget:

<snh-form-field
  snh-model="c.widget.options.shared.action"
  snh-name="action"/>

That updates the editor to now include a new aggregate column specification property called action. Of course, that doesn’t actually add any functionality to the Data Table widgets just yet, but at least now we can add a value to that property through the editor, which is a start. Now let’s take a look at the actual Data Table widgets and see what we need to do in order to leverage that new property.

Once again, the easiest place to start is with the HTML. Here is the current section of the HTML that deals with aggregate columns:

<td ng-repeat="obj in item.aggValue" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  {{obj.value}}
</td>

Let’s replace that with this:

<td ng-repeat="obj in item.aggValue" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  <a ng-if="obj.action" href="javascript:void(0)" ng-click="aggregateclick(obj.name, item)">{{obj.value}}</a>
  <span ng-if="!obj.action">{{obj.value}}</span>
</td>

Our new code now contains two mutually exclusive options, one an anchor and the other a span, which are rendered depending on whether or not there is a value in the new action property. The anchor tag does not directly launch the new page, which is only one of the possible responses to an action, but rather invokes a client-side script, passing the name of the aggregate column and the object containing the data for the row. Here is the code for this new client-side function:

$scope.aggregateclick = function(aggregate, item) {
	spNavStateManager.onRecordChange(c.data.table).then(function() {
		var parms = {};
		parms.table = c.data.table;
		parms.sys_id = item.sys_id;
		parms.record = item;
		for (var a in c.data.aggarray) {
			if (c.data.aggarray[a].name == aggregate) {
				parms.aggregate = c.data.aggarray[a];
			}
		}
		$scope.ignoreLocationChange = true;
		for (var x in c.data.list) {
			c.data.list[x].selected = false;
		}
		item.selected = true;
		$scope.$emit(eventNames.aggregateClick, parms);
		if (parms.aggregate.action && parms.aggregate.action != 'broadcast') {
			if (!parms.aggregate.action.startsWith('?')) {
				parms.aggregate.action = '?' + parms.aggregate.action;
			}
			if (parms.aggregate.action.indexOf('{{sys_id}}')) {
				parms.aggregate.action = parms.aggregate.action.replace('{{sys_id}}', parms.sys_id);
			}
			window.location.search = parms.aggregate.action;
		}
	}, function() {
		// do nothing in case of closing the modal by clicking on x
	});	
};

This function is basically a copy of the existing buttonclick function, adapted for use in the aggregate column section. In all cases where an action has been specified, the click information is broadcast to all of the other widgets on the page. Additionally, if the action is not the word broadcast, it is assumed to be a URL search string, and the window.location.search property is updated with the resolved value, invoking the new portal page.

The last thing that we need to do is to add a couple of new properties to the object returned by the getAggregateValue function on the server side. To support the new HTML, we just need to change this:

return {value: value};

… to this:

return {value: value, name: config.name, action: config.action};

That should do it for the changes. Now all we have to do is to build some test cases for both the new page and the pop-up dialog options to see if this all works. That sounds like a good project for our next installment.

SNH Data Table Widgets on Share

“The first time you do a thing is always exciting.”
Agatha Christie

I’ve never used Share before, but after completing the work on the Aggregate List Columns and bundling that work up with all of the other related projects and artifacts, I decided that I would go ahead and post the whole thing out there. I have always hesitated to throw stuff from here out there, mainly because most of the things that you will find on this site are not very well documented, at least not from the user’s perspective. But, I have considered doing it anyway on a number of occasions. I was pretty close to sharing the My Delegates Widget until I discovered that someone else had already beat me to it. I also thought about tossing out a number of other items such as the Dynamic Service Portal Breadcrumbs and the Service Portal Widget Help, but like quite a number of other things, those were just thoughts that never turned into any kind of action. This time, though, quite a number of things were all bundled together into a single Update Set, and I thought that maybe there just might be a thing or two buried in there somewhere that someone somewhere might find to be of value. We’ll see.

My other hesitation to posting this on Share was the fact that these are all Service Portal components, and ServiceNow has made it pretty clear that they would like to see folks abandon the Service Portal in favor of their latest approach to application development. While it may be true that the Service Portal is on the way out, it has been my experience that such transitions usually take some time to be fully realized, so there still may be an active Service Portal or two floating around out there for a while. Still, everyone always likes to jump on the new stuff, so the interest in Service Portal components is something that is bound to start dropping off over time. On the other hand, that actually serves as an argument for shoving it out there now, as waiting around would just mean even less relevance to the environment of the future.

Anyway, it’s done now. Share obviously has a much broader reach than this little blog, so it will be interesting to see if anyone happens to come across it out there with all of the other artifacts on the site. I did take a quick peek this morning, and it does look like a couple of brave souls have already hit the download button, but I don’t see any feedback posted as yet. That will probably take a little more time. Who knows; if it all works out, maybe one day I will throw something else out there. Only time will tell.

Aggregate List Columns, Part IX

“You have to finish things — that’s what you learn from, you learn by finishing things.”
Neil Gaiman

Last time, we attempted to wrap this whole thing up with the remaining modifications to the Content Selector Configuration Editor, but we ran into a problem with the code that we borrowed from the sn-record-picker Helper. Now that we have taken a quick detour to resolve that issue, we need to get back to our new pop-up aggregate column editor and apply the same fix to our pilfered code. As with the sn-record-picker Helper, we need to add some code to the server side to leverage the TableUtils object to get our list of tables in the extension hierarchy. Here is the new Server script, with basically the same function as the one that we added to the sn-record-picker Helper, with a few minor modifications due to the difference in our variable names.

(function() {
	if (input && input.tableName) {
		var tu = new TableUtils(input.tableName);
		var tableList = tu.getTables();
		data.tableList = j2js(tableList);
	}
})();

With that now in place, we can update the client-side buildFieldFilter function to call the server side to get the list, and then use that list to build the new filter.

$scope.buildFieldFilter = function() {
	c.data.fieldFilter = 'name=' + c.widget.options.shared.table.value;
	c.data.tableName = c.widget.options.shared.table.value;
	c.server.update().then(function(response) {
		if (response.tableList && Array.isArray(response.tableList) && response.tableList.length > 1) {
			c.data.fieldFilter = 'nameIN' + response.tableList.join(',');
		}
		c.data.fieldFilter += '^elementISNOTEMPTY^internal_type=reference';
	});
};

Now we just need to pop up the editor again and see if that actually fixes the problem that we were having earlier.

Pop-up aggregate column spec editor with field pick list corrected

That’s better! Now when I search for a field on the Incident table, I get to select from all of the fields on the table, not just the ones attached to the primary table. That’s what we wanted to see.

That takes care of the pop-up aggregate column specification editor that we cloned from the existing button/icon specification editor, so now all that is left for us to do is to tweak the code that actually saves the changes once all of the editing has been completed. The Save process actually rebuilds the entire script stored in the Script Include record, so we just need to add some code to create the aggregate column section for each table definition. Once again, we can leverage the existing buttons and icons code as a starting point, and then make the necessary changes to adapt it to use for aggregate columns. Here are the relevant lines from the Save() function in the widget’s Server script:

script += "',\n				btnarray: [";
var lastSeparator = '';
for (var b=0; b<tableTable[tableState.name].btnarray.length; b++) {
	var thisButton = tableTable[tableState.name].btnarray[b];
	script += lastSeparator;
	script += "{\n					name: '";
	script += thisButton.name;
	script += "',\n					label: '";
	script += thisButton.label;
	script += "',\n					heading: '";
	script += thisButton.heading;
	script += "',\n					icon: '";
	script += thisButton.icon;
	script += "',\n					color: '";
	script += thisButton.color;
	script += "',\n					hint: '";
	script += thisButton.hint;
	script += "',\n					page_id: '";
	script += thisButton.page_id;
	script += "'\n				}";
	lastSeparator = ",";
}
script += "]";

As we have done a number of times now, we can make a few global text replacements and alter a few variable names to align with our needs and come up with something that will work for aggregate columns specifications in very much the same way that it is currently working for buttons and icons.

script += "',\n				aggarray: [";
var lastSeparator = '';
for (var g=0; g<tableTable[tableState.name].aggarray.length; g++) {
	var thisAggregate = tableTable[tableState.name].aggarray[g];
	script += lastSeparator;
	script += "{\n					name: '";
	script += thisAggregate.name;
	script += "',\n					label: '";
	script += thisAggregate.label;
	script += "',\n					heading: '";
	script += thisAggregate.heading;
	script += "',\n					table: '";
	script += thisAggregate.table;
	script += "',\n					field: '";
	script += thisAggregate.field;
	script += "',\n					filter: '";
	script += thisAggregate.filter;
	script += "',\n					source: '";
	script += thisAggregate.source;
	script += "'\n				}";
	lastSeparator = ",";
}
script += "]";

And that’s all there is to that. That should be everything now, so all that is left to do is to bundle all of this into an Update Set so that folks can play along at home. This effort has been primarily focused on the components of the Customizing the Data Table Widget project, but it has also involved elements of the Configurable Data Table Widget Content Selector series as well as the Content Selector Configuration Editor. Additionally, many of the Service Portal pages that use these components, such as the Service Portal User Directory, also include the Dynamic Service Portal Breadcrumbs widget. Rather than create a new version for each and every one of these interrelated project, I think I will just lump everything together into a single Update Set and call it version 2.0 of the Customizing the Data Table Widget project. Since many of the widgets involved also utilize the Service Portal Form Fields, that will get pulled into the Update Set as well, and just for good measure, I think I will toss in the sn-record-picker Helper, too. That one is not actually directly related to all of the others, but we did steal some code from there, so there might be a few folks who may want to take a look at that one. You can download the whole lot from here. As always, if you have any comments, questions, concerns, or issues, please leave a comment below. All feedback is always welcome. And if you have made it this far, thanks for following along all the way to the end!

But wait … there’s more!

Aggregate List Columns, Part VIII

“We must accept responsibility for a problem before we can solve it. We cannot solve a problem by saying ‘It’s not my problem.’ We cannot solve a problem by hoping that someone else will solve it for us. I can solve a problem only when I say ‘This is my problem and it’s up to me to solve it.'”
M. Scott Peck

Last time, we completed the required modifications to the Configurable Data Table Widget Content Selector and started to tackle changes to the Content Selector Configuration Editor. Now we need to create a new Service Portal widget to handle the editing of new and existing aggregate column specifications. To begin, I cloned the existing Button/Icon Editor widget, and then dug into the HTML to see what it was that we had to work with.

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

Basically, it is just a DIV full of SNH Form Fields for all of the properties of a Button/Icon configuration. Since many of the properties of an Aggregate Column specification are the same as those of a Button/Icon configuration, we only need to make the changes for those that are different. Here is what I came up with for my initial version:

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

In looking through the rest of the code in the widget, it does not appear that there are any other changes necessary to make this work, so the next thing to do would be to fire up the modified Content Selector Configuration Editor and see how things look.

New pop-up editor for aggregate columns

We can make sure that it actually works by adding an ‘x’ to each value on the form and clicking on the Save button. That should update the values displayed on the main screen.

Modified aggregate column values after saving the edits

The reason that all seems to work, even though the only changes that we made were to the HTML, is that the main widget and the pop-up editor widget share a common object called, appropriately enough, shared. Clicking on the Save button simply blows away the pop-up widget and returns control to the parent widget, where the values entered on the pop-up form are still available in the shared object, even though the pop-up widget is now gone. The new editAggregate function in the parent widget that we copied from the existing editButton function creates and populates this shared object, and then when the pop-up widget closes, uses the values in the shared object to update the edited row.

$scope.editAggregate = function(aggregate, aggArray) {
	var shared = {};
	if (aggregate != 'new') {
		shared.label = aggregate.label;
		shared.name = aggregate.name;
		shared.heading = aggregate.heading;
		shared.table = aggregate.table;
		shared.field = aggregate.field;
		shared.filter = aggregate.filter;
		shared.source = aggregate.source;
	}
	spModal.open({
		title: 'Aggregate Column Editor',
		widget: 'aggregate-column-editor',
		shared: shared
	}).then(function() {
		if (aggregate == 'new') {
			aggregate = {};
			aggArray.push(aggregate);
		}
		aggregate.label = shared.label || '';
		aggregate.name = shared.name || '';
		aggregate.heading = shared.heading || '';
		aggregate.table = shared.table || '';
		aggregate.field = shared.field || '';
		aggregate.filter = shared.filter || '';
		aggregate.source = shared.source || '';
	});
};

At this point, this would all be good enough to get the job done, but for the table and field properties, it would be nice to have a pick list of tables and a pick list of reference fields present on that table rather than just open entry fields. We should be able to do that with a couple of sn-record-pickers, so I could fire up our old friend, the sn-record-picker Helper and configure them, but we can do even better than that. The sn-record-picker Helper is actually built with a bunch of of sn-record-pickers, and it already contains one for a list of tables and several for lists of fields dependent on the table selected. We should be able to just steal most of that in its entirety to get what we want. Here is the SNH Form Field for the table picker:

<snh-form-field
  snh-model="c.data.table"
  snh-name="table"
  snh-type="reference"
  snh-help="Select the ServiceNow database table that will contain the options from which the user will select their choice or choices."
  snh-change="buildFieldFilter();"
  snh-required="true"
  placeholder="Choose a Table"
  table="'sys_db_object'"
  display-field="'label'"
  display-fields="'name'"
  value-field="'name'"
  search-fields="'name,label'">
</snh-form-field>

With just a few simple modifications, we can adapt this to our needs:

<snh-form-field
  snh-model="c.widget.options.shared.table"
  snh-name="table"
  snh-type="reference"
  snh-change="buildFieldFilter();"
  snh-required="true"
  placeholder="Choose a Table"
  table="'sys_db_object'"
  display-field="'label'"
  display-fields="'name'"
  value-field="'name'"
  search-fields="'name,label'"/>

All I did here was to change the model to our model, drop the help (it would be helpful, but would make the pop-form way too long if all of the fields included help text), and dropped the closing tag in favor of a closing / at the end of the main tag. Easy peasy. Of course, we have now referenced a buildFieldFilter() function that doesn’t exist, but again, we should be able to just steal that from the sn-record-picker Helper widget. Here it is:

$scope.buildFieldFilter = function() {
	c.data.ready = false;
	c.data.fieldFilter = 'elementISNOTEMPTY^name=' + c.data.table.value;
};

We don’t need the c.data.ready indicator for our use case, so we can just snag the whole thing, delete that line, and update the table variable name to match the one that we are using in our widget. Again, pretty simple stuff.

Now we just need to do the same for the field picker. There are several of these in the sn-record-picker Helper widget, but a few them allow multiple selections, so we will want to pick one that is just a single select, such as the Primary Display Field.

<snh-form-field
  snh-model="c.data.displayField"
  snh-name="displayField"
  snh-label="Primary Display Field"
  snh-type="reference"
  snh-help="Select the primary display field."
  snh-required="true"
  snh-change="c.data.ready=false"
  placeholder="Choose a Field"
  table="'sys_dictionary'"
  display-field="'column_label'"
  display-fields="'element'"
  value-field="'element'"
  search-fields="'column_label,element'"
  default-query="c.data.fieldFilter">
</snh-form-field>

Here is my adaptation of that tag for our widget.

<snh-form-field
  snh-model="c.widget.options.shared.field"
  snh-name="field"
  snh-type="reference"
  snh-required="true"
  placeholder="Choose a Field"
  table="'sys_dictionary'"
  display-field="'column_label'"
  display-fields="'element,reference'"
  value-field="'element'"
  search-fields="'column_label,element'"
  default-query="c.data.fieldFilter"/>

That should take care of that. Let’s have a look and see what we have done.

New data pickers for the Table and Field fields

Well, that is really disturbing. Everything is working as intended as far as the relationship between the pickers is concerned, but when I went to select assigned_to from the drop-down, it was not on the list. It appears as if the fields on the list are limited to just those fields on the primary table, and the list does not include any of the fields for any of the tables from which that primary table may have been extended. This is particularly distressing news, as that means that the sn-record-picker Helper widget, from which we lifted this code, has had this issue all along. Not good! We are going to need to stop and fix that right away, and then we can revisit this guy next time out.

Aggregate List Columns, Part VII

“The most effective way to do it is to do it.”
Amelia Earhart

Last time, we completed the modifications to the last of the wrapper widgets, so now it is time to tackle the Content Selector Configuration Editor. Before we do that, though, we need to make one small addition to the Configurable Data Table Widget Content Selector itself. This widget builds the URL for the SNH Data Table from URL Definition widget, and so we need to add support for aggregate columns to the process that constructs that URL. I found these lines in the Client controller related to the buttons and icons:

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

So I just made a copy of those lines and then modified the copy to work with aggregates instead of buttons.

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

That was another simple and easy fix. Now onto the editor, which I am sure will be quite a bit more involved. Starting with the easy part, which I always like to do, we can take a look at the HTML and hunt for a section related to the buttons and icons, and then copy it to use as a base for creating a similar section for aggregate columns. Here is the relevant code:

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

Copying that code, making a few text replacements here and there, and then adjusting some of the columns to meet our needs resulted in the following new section of HTML, inserted above the buttons and icons section:

<div id="label.aggarray" class="snh-label" nowrap="true">
  <label for="aggarray" class="col-xs-12 col-md-4 col-lg-6 control-label">
    <span id="status.aggarray"></span>
    <span class="text-primary" title="Aggregate Columns" data-original-title="Aggregate Columns">${Aggregate Columns}</span>
  </label>
</div>
<table class="table table-hover table-condensed">
  <thead>
    <tr>
      <th style="text-align: center;">${Label}</th>
      <th style="text-align: center;">${Name}</th>
      <th style="text-align: center;">${Heading}</th>
      <th style="text-align: center;">${Table}</th>
      <th style="text-align: center;">${Field}</th>
      <th style="text-align: center;">${Filter}</th>
      <th style="text-align: center;">${Source}</th>
      <th style="text-align: center;">${Edit}</th>
      <th style="text-align: center;">${Delete}</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="agg in tbl[state.name].aggarray" ng-hide="agg.removed">
      <td data-th="${Label}">{{agg.label}}</td>
      <td data-th="${Name}">{{agg.name}}</td>
      <td data-th="${Heading}">{{agg.heading}}</td>
      <td data-th="${Icon Name}">{{agg.table}}</td>
      <td data-th="${Color}">{{agg.field}}</td>
      <td data-th="${Hint}">{{agg.filter}}</td>
      <td data-th="${Page}">{{agg.source}}</td>
      <td data-th="${Edit}" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editAggregate(agg)" alt="Click here to edit this Aggregate Column" title="Click here to edit this Aggregate Column" style="cursor: pointer;"/></td>
      <td data-th="${Delete}" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteAggregate(agg, tbl[state.name].aggarray)" alt="Click here to delete this Aggregate Column" title="Click here to delete this Aggregate Column" style="cursor: pointer;"/></td>
    </tr>
  </tbody>
</table>
<div style="width: 100%; text-align: right;">
  <button ng-click="editAggregate('new', tbl[state.name].aggarray, tbl);" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new Aggregate Column">Add a new Aggregate Column</button>
</div>

Now we have referenced a few nonexistent functions at this point, but that should not prevent us from pulling up the page and seeing how it looks so far. We can’t really click on anything in the new aggregates section at the moment, but let’s just take a peek and see how it all renders on the screen.

Configuration file selection screen

Let’s select our second test configuration, since that one has a source property defined, and see how things look.

New HTML section for aggregate column definitions

So far, so good. Now we need to create those missing client-side functions referenced in the new HTML, which we should be able to do relatively easily by copying corresponding functions used for the buttons and icons. Let’s start with the Delete function, since that one should be the easiest. Here is the code to delete a button configuration:

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

… and here is the modified copy that we can use for the aggregates section:

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

Next is the Edit function, which is a little more complicated. Once again, we can use the Edit function for the button configurations as a starting point.

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

As we did with the HTML, we can make a few text replacements here and there and then adjust some of the columns to come up with a suitable function for aggregate specifications.

$scope.editAggregate = function(aggregate, aggArray) {
	var shared = {};
	if (aggregate != 'new') {
		shared.label = aggregate.label;
		shared.name = aggregate.name;
		shared.heading = aggregate.heading;
		shared.table = aggregate.table;
		shared.field = aggregate.field;
		shared.filter = aggregate.filter;
		shared.source = aggregate.source;
	}
	spModal.open({
		title: 'Aggregate Column Editor',
		widget: 'aggregate-column-editor',
		shared: shared
	}).then(function() {
		if (aggregate == 'new') {
			aggregate = {};
			aggArray.push(aggregate);
		}
		aggregate.label = shared.label || '';
		aggregate.name = shared.name || '';
		aggregate.heading = shared.heading || '';
		aggregate.table = shared.table || '';
		aggregate.field = shared.field || '';
		aggregate.filter = shared.filter || '';
		aggregate.source = shared.source || '';
	});
};

That was relatively painless, but now we have code that references an entire widget that does not yet exist. Once again, we can create the missing widget by cloning the existing widget for the buttons and icons, but that seems like it might involve a little bit of work. Let’s jump into that in our next installment.

Aggregate List Columns, Part VI

“There are three qualities that make someone a true professional. These are the ability to work unsupervised, the ability to certify the completion of a job or task and, finally, the ability to behave with integrity at all times.”
Subroto Bagchi

Last time, we wrapped up the changes for the SNH Data Table from Instance Definition widget, and fixed a little issue with the core SNH Data Table widget in the process. Today, we need to do the same for the remaining wrapper widget, SNH Data Table from URL Definition. As with the previous widget, there isn’t that much code here that needs to be addressed, but I did come across these lines in the widget’s Server script:

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

It looks like I need to add the aggregates parameter to the list, so I threw that in and collapsed everything into a single line.

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

That was the only thing that I could find, so the next thing that I did was to clone the original test page once again and swap out the table widget for the one that I wanted to test. The problem with testing this one, though, is that all of the options that drive the process are URL parameters, and I did not want to type or paste in that huge URL every time I wanted to test. To eliminate that, I added an HTML widget above the table with a link that would contain the URL needed for testing. This way, all I needed to do to perform a test would be to click on that link.

Now I need to build a test URL using the same parameters that we used for the previous widget testing. Here is the list:

table: sys_user_group
filter: typeCONTAINS1cb8ab9bff500200158bffffffffff62
fields: name,manager
order_by: name
maximum_entries: 7
aggregates: [{ "label": "Members", "name": "members", "heading": "Members", "table": "sys_user_grmember", "field": "group", "filter": "user.active=true" },{ "label": "Incidents", "name": "incidents", "heading": "Incidents", "table": "incident", "field": "assignment_group", "filter": "active=true" },{ "label": "Catalog Tasks", "name": "sc_tasks", "heading": "Catalog Tasks", "table": "sc_task", "field": "assignment_group", "filter": "active=true" }]
refpage: {"sys_user": "user_profile"}

Using all of that to create a URL for the test page results in this lengthy string:

/sp?id=aggregate_test_4&table=sys_user_group&filter=typeCONTAINS1cb8ab9bff500200158bffffffffff62&fields=name,manager&order_by=name&maximum_entries=7&aggregates=[{"label":"Members","name":"members","heading":"Members","table":"sys_user_grmember","field":"group","filter":"user.active%3Dtrue"},{"label":"Incidents","name":"incidents","heading":"Incidents","table":"incident","field":"assignment_group","filter":"active%3Dtrue"},{"label":"CatalogTasks","name":"sc_tasks","heading":"CatalogTasks","table":"sc_task","field":"assignment_group","filter":"active%3Dtrue"}]&refpage={"sys_user":"user_profile"}

Once I constructed the URL, I added the following HTML to the HTML widget that I inserted on the page:

<div class="text-center" style="padding: 15px;">
  <a href="/sp?id=aggregate_test_4&amp;table=sys_user_group&amp;filter=typeCONTAINS1cb8ab9bff500200158bffffffffff62&amp;fields=name,manager&amp;order_by=name&amp;maximum_entries=7&amp;aggregates=[{&quot;label&quot;:&quot;Members&quot;,&quot;name&quot;:&quot;members&quot;,&quot;heading&quot;:&quot;Members&quot;,&quot;table&quot;:&quot;sys_user_grmember&quot;,&quot;field&quot;:&quot;group&quot;,&quot;filter&quot;:&quot;user.active%3Dtrue&quot;},{&quot;label&quot;:&quot;Incidents&quot;,&quot;name&quot;:&quot;incidents&quot;,&quot;heading&quot;:&quot;Incidents&quot;,&quot;table&quot;:&quot;incident&quot;,&quot;field&quot;:&quot;assignment_group&quot;,&quot;filter&quot;:&quot;active%3Dtrue&quot;},{&quot;label&quot;:&quot;CatalogTasks&quot;,&quot;name&quot;:&quot;sc_tasks&quot;,&quot;heading&quot;:&quot;CatalogTasks&quot;,&quot;table&quot;:&quot;sc_task&quot;,&quot;field&quot;:&quot;assignment_group&quot;,&quot;filter&quot;:&quot;active%3Dtrue&quot;}]&amp;refpage={&quot;sys_user&quot;:&quot;user_profile&quot;}">
    <button class="btn btn-primary">Click to Test</button>
  </a>
</div>

Now, all I needed to do was to bring up the Service Portal and pull up the page.

First look at the fourth test page

OK, that’s not all I needed to do. I also needed to click on the button to invoke that long URL with all of the parameters included.

Fourth test page after clicking on the Click to Test button

There, that’s better. So, it looks like everything seems to be in working order. That takes care of the last of the wrapper widgets. We still need to update the configuration file editor to support the new aggregate columns, which should be relatively straightforward, so maybe we can jump right into that next time out, wrap this whole thing up, and put out a new Update Set.

Aggregate List Columns, Part V

“The trouble you’re expecting never happens; it’s always something that sneaks up the other way.”
George R. Stewart

Last time, we took a little side trip on our journey to add this new functionality to the SNH Data Table Widget collection, but now we need to get back on track and finish up modifying the other wrapper widgets that still need to have some changes. Let’s start with the SNH Data Table from Instance Definition, which does not rely on an outside configuration object. When you configure this widget, you use the Edit feature of the Service Portal Page Designer. The edit dialog box that comes up can be customized for a widget using the Option schema field on the widget form, which already contains a number of customizations for the existing version of the widget. Here is how that looks right now:

[{"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"}]

In the current version, there is a string field for entering a JSON specification object for both Buttons/Icons and Reference Pages, so we will want to add one more just like that for our new aggregate columns. We can clean up the labels for all three while we are at it, just to make things a little clearer, so now it looks like this:

[{"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 specifications for aggregate data columns",
"name":"aggregates",
"default_value":"",
"section":"Behavior",
"label":"Aggregate Column Specifications (JSON)",
"type":"String"},
{"hint":"A JSON object containing the specifications for row-level buttons and action icons",
"name":"buttons",
"default_value":"",
"section":"Behavior",
"label":"Button/Icon Specifications (JSON)",
"type":"String"},
{"hint":"A JSON object containing the page id for any reference column links",
"name":"refpage",
"default_value":"",
"section":"Behavior",
"label":"Reference Page Specifications (JSON)",
"type":"String"}]

Looking over the rest of the code, it doesn’t look as if there is anything else that needs to be done, so the next thing that we need to do is to set up a test so that we can try it out. Once again, we can clone the original test page and then make our modifications to the cloned page. The first thing that we will need to do is to swap out the SNH Data Table from JSON Configuration widget with our modified SNH Data Table from Instance Definition widget. Once we do that, we can click on the Edit pencil in the Page Designer and fill out the modified form.

Top half of the Page Designer widget option editor

For this test, I decided to use the Group table instead of the User table, and for our little test, I decided to limit the list to just ITIL groups. The type field can be more than one type, though, so I had to use a CONTAINS filter rather than an = filter, and the values are sys_ids, not names, so I had to look up the sys_id for the ITIL type. I just added a couple of fields, Name and Manager, and then ordered the list by Name. That pretty much took care of the top half of the widget option editor, so then I scrolled down to the bottom half, where our customizations appear.

Bottom half of the Page Designer widget option editor

Here I left the default color, selected an appropriate glyph icon for a group, and left the Link to this page and Enable Filter fields intentionally blank. In the Aggregate Column Specifications field, I entered a JSON String defining three different aggregate columns:

[{
 "label": "Members",
 "name": "members",
 "heading": "Members",
 "table": "sys_user_grmember",
 "field": "group",
 "filter": "user.active=true"
},{
 "label": "Incidents",
 "name": "incidents",
 "heading": "Incidents",
 "table": "incident",
 "field": "assignment_group",
 "filter": "active=true"
},{
 "label": "Catalog Tasks",
 "name": "sc_tasks",
 "heading": "Catalog Tasks",
 "table": "sc_task",
 "field": "assignment_group",
 "filter": "active=true"
}]

I did not define any buttons or icons, but I did set up a reference configuration to send the links on the managers to the User Profile page. All that was left to do at that point was to Save the options, jump out to the Service Portal, and give this baby a try.

First test of the new widget modifications

Now that’s disturbing. There is no data in any of the aggregate columns. I hate it when that happens! Now what? Well, I always try to keep this in mind whenever these things suddenly crop up.

After quite a few trials and tribulations, I finally tracked down the source of the problem. The ng-repeat for the aggregate columns is actually nested underneath another ng-repeat for the rows in the table. Apparently, ng-repeat has some issues with arrays of primitives or strings, and prefers to deal with objects. Why that did not present itself when I was working with an array length of 1 earlier, and only now caused a problem with an array length of 3 is still a mystery to me. Fortunately, I never need to know or understand the why; I just need to know what to do about it. The solution was to convert my integer value to an object containing an integer value. So, in the core SNH Data Table widget, I changed this:

return value;

… to this:

return {value: value};

Of course, that meant that I also had to change the HTML accordingly, so I changed this:

<td ng-repeat="aggValue in item.aggValue" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  {{aggValue}}
</td>

… to this:

<td ng-repeat="obj in item.aggValue" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  {{obj.value}}
</td>

Now let’s have another look at that new test page.

Second test of the new widget modifications

That’s better! OK, so that takes care of wrapper widget #2. Now we just need to handle the third wrapper widget, SNH Data Table from URL Definition. That sounds like a good project for our next installment.

Aggregate List Columns, Part IV

“Everything can be improved.”
Clarence W. Barron

Last time, I promised that we would get into providing aggregate column support to the remaining data table wrapper widgets and the Content Selector Configuration Editor, but before we do that, there is one little extra feature that I would like insert. As you may recall, when I set up the filter for the initial test page, I hard-coded the list of sys_ids for the members of the Hardware group. It would have been much easier to use the sys_user_grmember table and set the filter to group.name=Hardware, but the problem with that would be that the sys_id of the row would then be the sys_id of the sys_user_grmember record, and not the sys_id of the sys_user record. The reason that that is a problem is because there are no Incidents assigned to sys_user_grmember records, so the aggregate column values would all be 0.

There is, however, a way to make that work. We just need to add one more property to our aggregate column configuration object. If we add an optional source property, then we can allow developers to configure an alternate source for the sys_id rather than just relying on the sys_id of the primary record of the row. It could be optional, as we could always default to the sys_id of the row if you did not specify an alternate source, but if you did enter a value, then the contents of that field would be the sys_id that we would use in place of the standard behavior. Using that approach, we could develop a configuration that could look like this:

name: 'sys_user_grmember',
displayName: 'User',
all: {
	filter: 'group.name=Hardware',
	fields: 'group,user,user.title,user.email,user.department,user.location',
	aggarray: [{
		label: 'Incidents',
		name: 'incidents',
		heading: 'Incidents',
		table: 'incident',
		field: 'assigned_to',
		filter: 'active=true',
		source: 'user'
	}],
	btnarray: [],
	refmap: {},
	actarray: []
}

Of course, in order to do that, we would have to add some additional logic inside the for loop that puts together the data to be sent to the getAggregateValue function, but the changes should not be that significant. I simply created a new variable called sysId, set it initially to the sys_id of the record, then checked to see if there was a source defined, and if so, overrode the initial value with the value found in the source field of the current record.

for (var j=0; j<data.aggarray.length; j++) {
	var config = data.aggarray[j];
	var sysId = record.sys_id;
	if (config.source) {
		sysId = gr.getValue(config.source);
	}
	record.aggValue.push(getAggregateValue(sysId, config));
}

That’s all there is to that. Now we just need to put together a test case to see if it will actually work. Rather than muck up my original test page, I decided to clone the page and the configuration script so that I could run tests both with and without a source defined. I called my copy of the configuration script AggregateTestConfig2, and it turned out like this:

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

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

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

	table: {
		all: [{
			name: 'sys_user_grmember',
			displayName: 'User',
			all: {
				filter: 'group.name=Hardware',
				fields: 'group,user,user.title,user.email,user.department,user.location',
				aggarray: [{
					label: 'Incidents',
					name: 'incidents',
					heading: 'Incidents',
					table: 'incident',
					field: 'assigned_to',
					filter: 'active=true',
					source: 'user'
				}],
				btnarray: [],
				refmap: {},
				actarray: []
			}
		}]
	},

	type: 'AggregateTestConfig2'
});

Now all we have to do is pull up the new test page and see how it looks.

Test page #2 with alternate sys_id source feature

So now you can define an alternate source for the sys_id that drives the aggregate, or you can choose not to and have the original functionality remain intact. Sweet! I like it. We still don’t have a way to click on the aggregate and pull up a list of the records represented there, but that’s something that we can always add in a future version. For now, we still have work to do involving the remainder of the core data table widget wrappers and the configuration file editor. I know that I said this last time, but this time for sure, we will jump right into that next time out.

Aggregate List Columns, Part III

“I want to try it to see what it’s like and see what my stuff looks like when I take it from inception to completion.”
Charlie Kaufman

Last time, we started the work of modifying the core widget of the data table widget collection. We took care of all of the code that imports the configuration data from the various wrapper widgets in the collection, and now we need to get down to the business of actually putting real data in the new aggregate list columns. To begin, we need to take a look at the code that pulls in the regular data for each row in the list.

	data.list = [];
	while (gr._next()) {
		var record = {};
		$sp.getRecordElements(record, gr, data.fields);
		if (typeof FilteredGlideRecord != 'undefined' && gr instanceof FilteredGlideRecord) {
			// FilteredGlideRecord doesn't do field-level
			// security, so take care of that here
			for (var f in data.fields_array) {
				var fld = data.fields_array[f];
				if (!gr.isValidField(fld))
					continue;

				if (!gr[fld].canRead()) {
					record[fld].value = null;
					record[fld].display_value = null;
				}
			}
		}
		for (var f in data.fields_array) {
			var fld = data.fields_array[f];
			if (record[fld].type == 'reference') {
				var refGr = gr;
				var refFld = fld;
				if (fld.indexOf('.') != -1) {
					var parts = fld.split('.');
					for (var x=0;x<parts.length-1;x++) {
						refGr = refGr[parts[x]].getRefRecord();
					}
					refFld = parts[parts.length-1];
				}
				if (refGr.isValidField(refFld)) {
					record[fld].table = refGr.getElement(refFld).getED().getReference();
					record[fld].record = {type: 'reference', sys_id: {value: record[fld].value, display_value: record[fld].value}, name: {value: record[fld].display_value, display_value: record[fld].display_value}};
				}
			}
		}
		record.sys_id = gr.getValue('sys_id');
		record.targetTable = gr.getRecordClassName();
		data.list.push(record);
	}

Basically, this code creates an array called data.list and then for each row creates an object called record, populates the record object from the GlideRecord, and then pushes the record object into the list. What we will want to do is add data to the record object before it gets pushed onto the list. The sys_id of the record will actually be useful to us, so it seems as if the best place to insert our code would be after the sys_id is established, but before the data.list.push(record) occurs. Here is what I came up with:

record.aggValue = [];
if (data.aggarray.length > 0) {
	for (var j=0; j<data.aggarray.length; j++) {
		record.aggValue.push(getAggregateValue(record.sys_id, data.aggarray[j]));
	}
}

To store the values, I added a value list to the record object. This is done whether or not any aggregate columns have been defined, just so there is something there regardless. Then we check to see if there actually are any aggregate columns defined, and if so, we loop through the definitions and then push a value onto the array for each definition. The value itself will be determined by a new function called getAggregateValue that takes the sys_id of the row and the aggregate column definition as arguments. We will need to build out that function, but for now, we can just return a hard-coded value, just to make sure that all is working before we dive into that.

function getAggregateValue(sys_id, config) {
	return 10;
}

Now that we have an array of values, we will need to go back into the HTML and modify the aggregate column section to pull data from this array. That section of the HTML now looks like this:

<td ng-repeat="aggValue in item.aggValue" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  {{aggValue}}
</td>

That should be enough to take it out for a spin and see if we broke anything. Let’s have a look.

Test page with hard-coded value array

Cool! So far, so good. Now we just need to go back into that getAggregateValue function definition and replace the hard-code value of 10 with some actual logic to pull the real data out of the database. For that, we will use a GlideAggregate on the configured table using the configured field and the sys_id of the current row. And if there is an optional filter present, we simply concatenate that to the end of the primary query.

function getAggregateValue(sys_id, config) {
	var value = 0;
	var ga = new GlideAggregate(config.table);
	ga.addAggregate('COUNT');
	var query = config.field + '=' + sys_id;
	if (config.filter) {
		query += '^' + config.filter;
	}
	ga.addEncodedQuery(query);
	ga.query();
	if (ga.next()) {
		value = parseInt(ga.getAggregate('COUNT'));
	}
	return value;
}

Now we can take another quick look and see if that finally gets us what we have been after all along.

Finally, the result we have been looking for

Voila! There it is — a column containing a count of selected related records. Now that wasn’t so hard, was it? Of course, we are not done quite yet. The SNH Data Table from JSON Configuration widget is not the only wrapper widget in the collection. We will need to add support for aggregate columns to the SNH Data Table from Instance Definition widget, as well as the SNH Data Table from URL Definition widget, the widget designed to work with the Configurable Data Table Widget Content Selector. Also, we have the Content Selector Configuration Editor, which allows you to create the JSON configuration files for both the Configurable Data Table Widget Content Selector and the SNH Data Table from JSON Configuration widget. That will have to be modified to support aggregate column specifications as well. None of that is super challenging now that we have things working, but it all needs to be done, so we will jump right into that next time out.

Aggregate List Columns, Part II

“If you have an apple and I have an apple and we exchange these apples then you and I will still each have one apple. But if you have an idea and I have an idea and we exchange these ideas, then each of us will have two ideas.”
George Bernard Shaw

Last time, we introduced the idea of a new column type for our modified version of the Service Portal Data Table Widget, built a sample configuration object, and then built a test page to try it all out. That was all relatively simple to do, but now we need to start digging around under the hood and see what coding changes we will need to make to the various table widgets in order to make this work. Our new test page uses the SNH Data Table from JSON Configuration widget, so that’s a good place to start looking for places that will need to be updated.

Virtually all of the heavy lifting for the data table widget collection is done in the core widget, SNH Data Table. All of the other widgets in the collection just provide a thin veneer over the top to gather up the configuration data from different sources. The SNH Data Table from JSON Configuration widget is no exception, and in digging through the code, it looks like there is really only one small place in the Server script that will need a slight modification.

// widget parameters
data.table_label = gr.getLabel();
data.filter = data.tableData.filter;
data.fields = data.tableData.fields;
data.btnarray = data.tableData.btnarray;
data.refmap = data.tableData.refmap;
data.actarray = data.tableData.actarray;

The above code moves each section of the configuration individually, and since we have added a new section to the configuration, we will need to insert a new line to include that new section. Copying the btnarray line to create a new aggarray line should take care of that.

// widget parameters
data.table_label = gr.getLabel();
data.filter = data.tableData.filter;
data.fields = data.tableData.fields;
data.aggarray = data.tableData.aggarray;
data.btnarray = data.tableData.btnarray;
data.refmap = data.tableData.refmap;
data.actarray = data.tableData.actarray;

One more easy part checked off of the To Do list. Now we need to take a look at that core widget, where we are going to find the bulk of the work to make this all happen. Still, we can start with the easy stuff, which will be found in the HTML portion of the widget. There are two areas on which we will need to focus, the column headings and the data rows. Once again, we can take a look at what is being done with the button array data and pretty much copy it verbatim to handle the new aggregate array data. Here is the relevant section for the column headings:

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

Inserting a copy of that code right above it and replacing the button references with the equivalent aggregate references yields the following new block of code:

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

A little bit lower in the HTML is the code for the individual rows. Here again, we can take a look at the existing code dedicated to the buttons and icons.

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

Since we have not done any work on gathering up any actual data at this point, we can skip the value portion of the cell for now and just throw in a hard-coded zero so that we can fire it up and take a look. Later, once we figure out how to insert the values, we can come back around and clean that up a bit, but for now, we just want to be able to pull up the page and see how the UI portion comes out on the screen. So our temporary code for the aggregate columns will start out looking like this:

<td ng-repeat="aggregate in data.aggarray" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  0
</td>

That should be enough to take a quick peek and see how things are working out. Here is the new test page with the column headings and columns, but no actual data.

Test page with column headings and mocked-up data

Beautiful! That takes care of another one of the easy parts. Now we are going to have to dig a little deeper and start figuring out how we can replace those zeroes with some real data. To begin, I got into the Server script section of the widget and did a Find on the word btnarray. My thought here was that anywhere there was code related to the buttons and icons, there should probably be similar code for the aggregates. The first thing that I came across was this comment:

* data.btnarray = the array of button specifications

So, I added a new comment right above that one.

* data.aggarray = the array of aggregate column specifications

The next thing I found was this line used to copy in all of the widget options:

	// copy to data[name] from input[name] || options[name]
	optCopy(['table', 'table_name', 'buttons', 'btns', 'refpage', 'bulkactions', 'btnarray', 'refmap', 'actarray',
		'p', 'o', 'd', 'filter', 'filterACLs', 'fields', 'field_list', 'keywords', 'view', 'relationship_id',
		'apply_to', 'apply_to_sys_id', 'window_size', 'show_breadcrumbs']);

… which I modified to be this:

	// copy to data[name] from input[name] || options[name]
	optCopy(['table', 'table_name', 'aggregates', 'buttons', 'btns', 'refpage',
		'bulkactions', 'aggarray', 'btnarray', 'refmap', 'actarray', 'p', 'o', 'd',
		'filter', 'filterACLs', 'fields', 'field_list', 'keywords', 'view',
		'relationship_id', 'apply_to', 'apply_to_sys_id', 'window_size',
		'show_breadcrumbs']);

Then I came across this block of code:

	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 {
		if (!data.btnarray) {
			data.btnarray = [];
		}
	}

So, I copied that and altered the copy to be this:

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

And that was it. None of that, of course, has anything to do with calculating the values for the new aggregate columns, but we won’t find that code by hunting for the btnarray variable. We are going to have to root around in the code that fetches the records to figure out where we need to add some logic to get the new values. That sounds like a good place to start in our next installment.