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.