Refactoring the SNH Data Table Widget, Part III

“More than the act of testing, the act of designing tests is one of the best bug preventers known.”
Boris Beizer

Now that we have refactored the various SNH Data Table widgets and added the missing support for clickable aggregate columns and conditional buttons and icons, it’s time to run everything through its paces and make sure that it all works. To begin, let’s just see if the stuff that was working is still working now that we have made all of these changes to the artifacts and added these new features. A good place to start would be with the User Directory, as that little project utilizes a number of different components that were affected by the changes.

Primary User Directory page

Well, that seems to work, still. Changing perspectives and selecting different states all seem to function as they should as well, as does paging through the data and sorting on the various columns. So far so good. This tests out the SNH Data Table from URL Definition widget as well as the underlying core SNH Data Table widget. In the User Directory, the Department column and the Location column reference links are mapped to two other pages that were built using the SNH Data Table from JSON Configuration widget, and clicking on the value in one of those columns should test out both the reference link handling and that other wrapper widget.

Department Roster page

So far, so good. Clicking on a Location value should test out the reference link handling from this wrapper widget, and also bring up yet another page using the second of the three wrapper widgets.

Location Roster page

Once again, everything seems to be behaving as it should using the two wrapper widgets tested so far. The third wrapper widget, the SNH Data Table from Instance Definition widget, is not used in the User Directory, but we have other test scenarios set up for that one. Before we move on, though we can go ahead and test one more thing while we are working with the User Directory: conditional icons. In the User Admin perspective, there is an icon defined to edit the user’s data. We can add a quick condition to that icon configuration and see how that works out. Just for testing purposes, let’s try the following expression:

item.department.display_value == 'Sales'

If that works, the edit icon should only appear for users who are assigned to the Sales department.

User Directory User Admin perspective with conditional icon

Well, that seems to work as well. Good deal. While we are here, we might as well click on the icon and see if the button click handling works as well as the reference link handling that we tested earlier.

User Profile edit page

And it would seem that it does. That’s about all that we can squeeze out of the User Directory, but we still have a lot more to test, including the third wrapper widget and the other new feature, the clickable aggregate columns. Plus, we still have to do regression testing for bulk actions and clicks that result in a modal pop-up box instead of branching to a new portal page. That’s a lot to get through, so let’s keep plowing ahead.

Our third aggregate column test page was built on the SNH Data Table from Instance Definition widget, so let’s pull that guy up and see how things look.

Aggregate Column test page #3

Well, that all seems to work, and the Network group has two Incidents, so we just need another page to which we can link to display those Incidents. That’s easy enough to do with yet another aggregate column test page using the SNH Data Table from Instance Definition widget, a table name of Incident and a filter of active=true^assignment_group={{sys_id}}.

Aggregate Column test page #5

So all of three of the wrapper widgets seem to work, which by default tests out the core widget, and direct links from reference columns, buttons, and aggregate columns all navigate to the appropriate pages. That just leaves bulk actions and modal pop-ups to be tested, both of which will require companion widgets to listen for the associated broadcast messages. For that, we will have to hunt down or create some companion widgets for testing purposes, which might get a little involved, so let’s jump into that next time out.

Conditional Buttons and Icons on the SNH Data Table Widget

“Become a beacon of enhancement, and then, when the night is gray, all of the boats will move towards you, bringing their bountiful riches.”
James Altucher

After I posted the SNH Data Table widget collection out on Share, there were a couple of things that I felt were missing in the version that I put out there. One of those was the ability to click on an aggregate column and pull up a list of the records represented by that value. I took care of that one here, but after having second thoughts, the version that I ended up with is not quite like the one described in that installment of the series. Still, after a litter refactoring, I got it working in a way that finally seemed to be acceptable. But there is still one more thing that I would like to add before I release another version of the collection: the ability to control the presence of a button or icon on the row based on some condition.

Right now, if you configure a button or icon, that button or icon appears on every row of the table. That’s a nice feature that the stock Data Table widget does not include, but it would be even nicer if you could control whether or not the button appeared based on some condition. My thought was that I could add yet one more property to the button/icon configuration object called condition that could be used to control the presence of the action item on the row. I wasn’t exactly sure how to make that happen, but as usual, I thought that I would tackle the easiest portion first, which would be to modify the Content Selector Configurator widget to include that additional property. We just went through that exercise when adding the action property to the aggregate column configuration object (which was later replaced with the hint and page_id properties during refactoring), so the exercise is virtually the same.

As we did with the new aggregate column configuration property, we can start with the main widget and add the following to the list of column headings for the buttons and icons section of the table specification:

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

And then in the repeating rows of that table, we can insert this line:

<td data-th="${Condition}">{{btn.condition}}<

On the client side, we can add the following line to the new record section of the editButton() function:

shared.condition = button.condition;

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

button.condition = shared.condition || '';

Finally, on the server side, in the Save() function that rebuilds the script, let’s add these lines in the button/icon specification section:

script += "',\n                 condition: '";
script += thisButton.condition;

That will handle things in most cases, but since this particular property is a Javascript expression, we need to account for the fact that the value might contain single quotes, and surrounding a value containing single quotes with single quotes will result in a syntax error in our Script Include. We should at least check for that, and if single quotes are present in the value, we should escape them. Instead of the script fragment above, which has been working for us in all other cases, let’s expand that a little bit to accommodate our concerns to something like this:

script += "',\n					condition: '";
if (thisButton.condition) {
	var condition = thisButton.condition;
	if (condition.indexOf("'") != -1) {
		condition = condition.replace(/'/g, "\\'");
	}
	script += condition;
}

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 this line to the HTML of the Button/Icon Editor widget:

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

That updates the editor to now include a new button/icon specification property called condition. 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 SNH Data Table widget 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 buttons and icons:

<td ng-repeat="button in data.btnarray" role="cell" 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>

In the current version, there are two mutually exclusive anchor tags controlled by ng-if attributes that look to see whether or not an icon image was specified. We should be able to logically and our new condition to the existing conditions without disturbing the rest of the existing structure. The easiest way to do that at this point would simply be to call a function that dealt with the condition and have it return true or false based on the contents of our new condition property. We will have to build that function, but for now, we can just assume that it exists and modify the above to now look like this:

<td ng-repeat="button in data.btnarray" role="cell" class="text-nowrap center" ng-class="{selected: item.selected}" tabindex="0">
  <a ng-if="!button.icon && buttonCondition(button.condition, item)" 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 && buttonCondition(button.condition, item)" 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>

That should take care of the HTML. Now we need to come up with a function that will do what we want to do, which is to return true if we want the button/icon to appear and false if we do not. We can start out with something like this:

$scope.buttonCondition = function(expression, item) {
	var response = true;
	if (expression) {
		// check to see if the condition is true or false
	}
	return response;
};

This defaults the response to true, and if there is no condition specified, then the response will be true. If there is a condition, then we need to see if that condition is true for this item. Basically, we want to run the code that is stored in the property value. For that, we can use $scope.$eval. This AngularJS function will run the expression and return the result, which will then become our response. That will make our function look like this:

$scope.buttonCondition = function(expression, item) {
	var response = true;
	if (expression) {
		response = $scope.$eval(expression, this);
	}
	return response;
};

And that should be that! Now all we need to do is test all of this out, plus do a whole lot of regression testing for all of the refactoring, and then we can finally put out a new Update Set. That is definitely quite a bit of work in an of itself, so let’s save all of that for a future installment.

Refactoring the SNH Data Table Widget, Part II

“An intuitive definition is that a safe refactoring is one that doesn’t break a program. Because a refactoring is intended to restructure a program without changing its behavior, a program should perform the same way after a refactoring as it does before.”
Martin Fowler

Last time, we began the work of cleaning up the SNH Data Table widgets by consolidating all of the added action functions and bringing in the latest version of the original widget. To complete the work, we need to do the same for the remaining wrapper widgets in the collection, two of which were cloned from existing stock components, with the third being a new addition having no original source (although it was actually cloned from the modified version of one of the other two). As usual, we will start with the easy one first, the SNH Data Table from Instance Definition, cloned from the stock Data Table from Instance Definition widget.

The biggest change here was the addition of four new options to allow the entry of JSON strings for configuring each of the four new features:

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

Other than that, the only other modification to the code required was in the Server script where we needed to point to the SNH core widget instead of the stock core widget:

// Start: SNH Data Table enhancements
	data.dataTableWidget = $sp.getWidget('snh-data-table', options);
// End: SNH Data Table enhancements

Aside from that single line, the remainder of the widget, including the entire Client script, remains the same as the latest version of the original widget. Next, we need to take a look at the SNH Data Table from URL Definition, which was cloned from the stock Data Table from URL Definition widget. As with the previous widget, the Client script from the latest version of the source widget remains the same. However, the Server script needs a little more modification than just the ID of the embedded core widget.

// Start: SNH Data Table enhancements
	data.fields = $sp.getParameter('fields') || $sp.getListColumns(data.table, data.view);
	copyParameters(data, ['aggregates', 'buttons', 'refpage', 'bulkactions']);
	data.show_new = options.show_new == true || options.show_new == "true";
	data.show_breadcrumbs = options.show_breadcrumbs == true || options.show_breadcrumbs == "true";
	data.window_size = $sp.getParameter('maximum_entries');
	data.btns = data.buttons;
	data.dataTableWidget = $sp.getWidget('snh-data-table', data);
// End: SNH Data Table enhancements

I left the original code intact, even in areas where the new code reset the values established differently in the original code, mainly because that didn’t really hurt anything and I was trying to retain the original as closely as possible to the way that it was for future comparisons.

That leaves the SNH Data Table from JSON Configuration, which does not have a stock version, although I did clone it originally from the modified SNH Data Table from URL Definition widget. Since there were no changes needed in the Client script of the other two widgets cloned from stock widgets, I went ahead and just copied the latest version of the Client script from the stock Data Table from URL Definition widget and pasted it into he Client script of the SNH Data Table from JSON Configuration widget. The rest was mostly custom code anyway, so I just left that alone.

That takes care of the three wrapper widgets, so now everything has been brought up the latest versions and the code for handling the four added features has all be consolidated into the core widget using a common function. That cleaned things up quite nicely, but I’m still not quite ready to spin up a new Update Set just yet. There is one more thing that I think needs to addressed before we do that.

Refactoring the SNH Data Table Widget

“Quality is never an accident; it is always the result of high intention, sincere effort, intelligent direction and skillful execution; it represents the wise choice of many alternatives.”
William A Foster

After I posted the SNH Data Table widget collection out on Share, I realized that there were a couple more features that really needed to be in there before it could truly be considered complete, so I started working on adding one of those features, the ability to click on an aggregate column value and see the records represented by that number. While testing out those new additions to the code, I discovered that I was not being consistent in the way in which these various features were being implemented, and decided to take a step back and review the entire set of additional features as a group. I don’t like to see one feature implemented one way and another, similar feature implemented some other way. Just to review, the SNH version of the Data Table widget adds the following configurable features:

  • Reference Field Links – For any field with a type of reference, the value is rendered as a link which can branch to another Portal Page or broadcast the details to a companion widget sharing the page.
  • Aggregate Columns – Aggregate columns are counts of related records, which can also now be rendered as a link which can branch to another Portal Page or broadcast the details to a companion widget sharing the page.
  • Buttons and Icons – Buttons and icons can also be configured, and clicking on a button or icon will branch to another Portal Page or broadcast the details to a companion widget sharing the page.
  • Bulk Actions – When one or more bulk actions are configured, checkboxes will be rendered at the beginning of each line, and a drop-down select list of actions will be rendered in the footer. Checking one or more rows of the table and selecting an action from the drop-down will branch to another Portal Page or broadcast the details to a companion widget sharing the page.

Although each of these features were unique, and developed for a specific purpose over a period of time, they all share a number of characteristics in common, particularly the fact that selecting an item will broadcast a message alerting other widgets of the selection and/or branch out to a different page on the portal. It seemed to me that, even though each feature had its own way of approaching this, and even though they all seemed to work just the way that they were, they all really needed to approach things in a consistent manner. So I decided to go back and review the various implementations and see if I could come up with a way for all of them to be handled in the same manner.

For the reference field links, I had leveraged the existing go() function from the original widget. For the buttons and icons, I had built a new function, which I also did later for the aggregate columns, but using a different approach. For the bulk actions, I also had a separate function, but it is a little different than the other three, as it deals with multiple records and the others all deal with a single record. Still, all four had some very similar characteristics.

The original go() function did a $scope.emit(), and then code in the wrapper widgets listened for that event and did the actual branching to the new page. I don’t want to disturb that existing code, as I would like the enhanced widget to be drop-in compatible with the original, but I don’t really see the need to involve the wrapper widgets in any of the things that I was doing. I had already done that in a few places by copying what was already there, but now that I look at it a little more closely, I would like to rip all of that out and handle everything in the core widget alone.

The easiest way to make sure that everyone uses the same approach is for everyone to share the same code. So I came up with a universal function that would take care of the broadcasting and the navigation for all four use cases, which turned out like this:

function processClick(sys_id, eventName, config, item) {
	spNavStateManager.onRecordChange(c.data.table).then(function() {
		var parms = {};
		parms.table = config.table || c.data.table;
		parms.sys_id = sys_id;
		parms.record = item;
		parms.config = config;
		$scope.ignoreLocationChange = true;
		for (var x in c.data.list) {
			c.data.list[x].selected = false;
		}
		$rootScope.$broadcast(eventName, parms);
		if (config.page_id) {
			var s = {id: config.page_id, table: parms.table, sys_id: parms.sys_id, view: $scope.data.view};
			var newURL = $location.search(s);
			spAriaFocusManager.navigateToLink(newURL.url());
		}
	});
}

The function accepts four arguments, sys_id, eventName, config, and item. The sys_id is the sys_id of the record, either the related record or the record of the active row. The eventName is one of the following:

referenceClick: 'data_table.referenceClick',
aggregateClick: 'data_table.aggregateClick',
buttonClick: 'data_table.buttonClick',
bulkAction: 'data_table.bulkAction'

The config is the configuration object for the reference, aggregate, button, icon, or bulk action, and the item is either the record for the current row, the related record, or in the case of bulk actions, the list of selected records. The function extracts certain elements from the passed arguments, broadcasts the message, and then if there is a portal page specified, branches to that page with URL parameters extracted from the data passed. This will now occur in the same manner, using the same technique for all four of the added features.

To invoke this function, I created four new functions, one for each of the four features. These functions establish the arguments in accordance with the needs of each feature. Here are the four new functions:

$scope.referenceClick = function(field, item) {
	var config = {};
	config.table = item[field].table;
	config.page_id = c.data.refmap[config.table] || 'form';
	var sys_id = item[field].value;
	processClick(sys_id, eventNames.referenceClick, config, item[field].record);
};

$scope.aggregateClick = function(aggregate, item) {
	var config = {};
	for (var a in c.data.aggarray) {
		if (c.data.aggarray[a].name == aggregate) {
			config = c.data.aggarray[a];
		}
	}
	var sys_id = item.sys_id;
	if (config.source && item[config.source].value) {
		sys_id = item[config.source].value;
	}
	processClick(sys_id, eventNames.aggregateClick, config, item);
};

$scope.buttonClick = function(button, item) {
	var config = {};
	for (var b in c.data.btnarray) {
		if (c.data.btnarray[b].name == button) {
			config = c.data.btnarray[b];
		}
	}
	processClick(item.sys_id, eventNames.buttonClick, config, item);
};

$scope.bulkActionSelected = function() {
	if (c.data.bulk_action) {
		var selected = [];
		for (var x in c.data.list) {
			if (c.data.list[x].selected) {
				selected.push(c.data.list[x]);
			}
		}
		if (parms.selected.length > 0) {
			var config = {};
			for (var b in c.data.actarray) {
				if (c.data.actarray[b].name == c.data.bulk_action) {
					config = c.data.actarray[b];
				}
			}
			config.table = c.data.table
			processClick('', eventNames.bulkAction, config, selected)
		} else {
			spModal.alert('You must select at least one row for this action');
		}
	}
	c.data.bulk_action = '';
};

In addition to wanting to consistently handle the various actions, I also wanted to run out and get the latest copy of the original Data Table widget to make sure that I had not missed any useful updates since I initially cloned the widget. At the same time, I wanted to start fresh with the latest copy and then surgically insert the enhancements, disturbing the original code as little as possible, and marking my additions with comments to clearly delineate the new code from the original. Here is how that turned out with the HTML template:

<div class="panel panel-{{options.color}} b" ng-class="{'data-table-high-contrast': accessibilityModeEnabled}">
    <div class="panel-heading form-inline" ng-hide="options.hide_header">
      <span class="dropdown m-r-xs">
        <button aria-label="{{data.title || data.table_plural}} ${Context Menu}" class="btn dropdown-toggle glyphicon glyphicon-menu-hamburger" style="line-height: 1.4em" id="optionsMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
        <ul class="dropdown-menu" aria-labelledby="optionsMenu">
          <li ng-repeat="t in ::exportTypes">
            <a ng-if="!tinyUrlEnabled" ng-href="/{{data.table}}_list.do?{{::t.value}}&sysparm_query={{data.exportQueryEncoded}}&sysparm_view={{data.view}}&sysparm_fields={{data.fields}}" target="_new" tabindex="-1">${Export as} {{::t.label}}</a>
            <a ng-if="tinyUrlEnabled" ng-href="/{{data.table}}_list.do?{{::t.value}}&sysparm_tiny={{tinyUrl}}" target="_new" tabindex="-1">${Export as} {{::t.label}}</a>
          </li>
        </ul>
      </span>
      <h2 class="panel-title" style="display:inline"><i ng-if="options.glyph" class="fa fa-{{options.glyph}} m-r"></i>{{data.title || data.table_plural}}<span class="sr-only">${table} - ${page} {{data.p}}</span></h2>
      <button name="new" role="button" class="btn btn-primary btn-sm m-l-xs" ng-click="newRecord()" ng-if="options.show_new && data.canCreate && !data.newButtonUnsupported" aria-label="${Create new record}">${New}</button>
      <div class="pull-right" ng-if="options.show_keywords">
		<form ng-if="data.hasTextIndex" ng-submit="setSearch(true)">
        <div class="input-group" role="presentation">
          <input type="text" name="datatable-search" ng-model="data.keywords" ng-model-options="{debounce:250}" class="form-control" placeholder="${Keyword Search}" aria-label="${Keyword Search}">
          <span class="input-group-btn">
            <button name="search" class="btn btn-default" type="submit" aria-label="${Search}"><span class="glyphicon glyphicon-search"></span></button>
          </span>
        </div>
        </form>
      </div>
      <div class="clearfix"></div>
    </div>
    <!-- body -->
    <div class="panel-body">
      <div ng-if="options.show_breadcrumbs && (data.filter || data.enable_filter)" class="filter-breadcrumbs">
	    		<sp-widget widget="data.filterBreadcrumbs"></sp-widget>
      </div>
      <div class="clearfix"></div>
      <div class="alert alert-info" ng-if="!data.list.length && !data.num_pages && !data.invalid_table && !loadingData">
        ${No records in {{data.table_label}} <span ng-if="data.filter">using that filter</span>}
      </div>
      <div class="alert alert-info" ng-if="loadingData">
          <fa name="spinner" spin="true"></fa> ${Loading data}...
       </div>
      <table class="table table-striped table-responsive" ng-if="data.list.length">
        <caption class="sr-only">{{data.title || data.table_plural}}</caption>
        <thead>
          <tr>
<!-- Start: SNH Data Table enhancements -->
            <th ng-if="data.actarray.length > 0" class="text-nowrap center" tabindex="0">
              <input type="checkbox" ng-model="data.master_checkbox" ng-click="masterCheckBoxClick();"/>
            </th>
<!-- End: SNH Data Table enhancements -->
            <th ng-repeat="field in data.fields_array track by $index" class="text-nowrap" ng-click="setOrderBy(field)"
             scope="col" role="columnheader" aria-sort="{{field == data.o ? (data.d == 'asc'? 'ascending': 'descending') : 'none'}}">
              <div class="th-title" title="${Sort by} {{field == data.o ? (data.d == 'asc' ?  '${Descending}': '${Ascending}') : '${Ascending}'}}" role="button" tabindex="0" aria-label="{{data.column_labels[field]}}">{{data.column_labels[field]}}</div>
              <i class="fa" ng-if="field == data.o" ng-class="{'asc': 'fa-chevron-up', 'desc': 'fa-chevron-down'}[data.d]"></i>
            </th>
<!-- Start: SNH Data Table enhancements -->
            <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>
<!-- End: SNH Data Table enhancements -->
          </tr>
        </thead>
        <tbody>
        <tr ng-repeat="item in data.list track by item.sys_id">
<!-- Start: SNH Data Table enhancements -->
           <td ng-if="data.actarray.length > 0" role="cell" class="text-nowrap center" tabindex="0">
              <input type="checkbox" ng-model="item.selected"/>
            </td>
            <td role="{{$first ? 'rowheader' : 'cell'}}" class="sp-list-cell" ng-class="{selected: item.selected}" ng-repeat="field in ::data.fields_array" data-field="{{::field}}" data-th="{{::data.column_labels[field]}}">
              <sn-avatar ng-if="item[field].value && item[field].type == 'reference' && item[field].table == 'sys_user'" primary="item[field].value" class="avatar-small" show-presence="true" enable-context-menu="false"></sn-avatar>
              <a ng-if="$first" href="javascript:void(0)" ng-click="go(item.targetTable, item)" aria-label="${Open record}: {{::item[field].display_value}}">{{::item[field].display_value | limitTo : item[field].limit}}{{::item[field].display_value.length > item[field].limit ? '...' : ''}}</a>
              <a ng-if="!$first && item[field].type == 'reference' && item[field].value" href="javascript:void(0)" ng-click="referenceClick(field, item)" aria-label="${Click for more on }{{::item[field].display_value}}">{{::item[field].display_value | limitTo : item[field].limit}}{{::item[field].display_value.length > item[field].limit ? '...' : ''}}</a>
              <span ng-if="!$first && item[field].type != 'reference'">{{::item[field].display_value | limitTo : item[field].limit}}{{::item[field].display_value.length > item[field].limit ? '...' : ''}}</span>
            </td>
            <td ng-repeat="obj in item.aggValue" role="cell" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
              <a ng-if="obj.name" href="javascript:void(0)" ng-click="aggregateClick(obj.name, item)">{{obj.value}}</a>
              <span ng-if="!obj.name">{{obj.value}}</span>
            </td>
            <td ng-repeat="button in data.btnarray" role="cell" 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>
<!-- End: SNH Data Table enhancements -->
          </tr>
        </tbody>
      </table>
      <div ng-class="{'pruned-msg-filter-pad': (!options.show_breadcrumbs || !data.filter) && !data.list.length}" class="pruned-msg" ng-if="rowsWerePruned()">
        <span ng-if="rowsPruned == 1">${{{rowsPruned}} row removed by security constraints}</span>
        <span ng-if="rowsPruned > 1">${{{rowsPruned}} rows removed by security constraints}</span>
      </div>
    </div>
    <!-- footer -->
 <!-- Start: SNH Data Table enhancements -->
    <div class="panel-footer" ng-if="data.actarray.length > 0 && data.row_count">
      <div class="btn-toolbar m-r pull-left">
        <select class="form-control" ng-model="data.bulk_action" ng-click="bulkActionSelected();">
          <option value="">${Actions on selected rows ...}</option>
          <option ng-repeat="opt in data.actarray" value="{{opt.name}}">{{opt.label}}</option>
        </select>
      </div>
      <span class="clearfix"></span>
    </div>
<!-- End: SNH Data Table enhancements -->
    <div class="panel-footer" ng-hide="options.hide_footer" ng-if="data.row_count" role="navigation" aria-label="${Pagination}">
      <div class="btn-toolbar m-r pull-left">
        <div class="btn-group">
          <a href="javascript:void(0)" ng-click="setPageNum(data.p - 1)" ng-class="{'disabled': data.p == 1}" class="btn btn-default" aria-label="${Previous page} {{data.p == 1 ? '${disabled}' : ''}}" tabindex="{{(data.p == 1) ? -1 : 0}}"><i class="fa fa-chevron-left"></i></a>
        </div>
        <div ng-if="data.num_pages > 1 && data.num_pages < 20" class="btn-group">
          <a ng-repeat="i in getNumber(data.num_pages) track by $index" ng-click="setPageNum($index + 1)" href="javascript:void(0)" ng-class="{active: ($index + 1) == data.p}" type="button" class="btn btn-default" aria-label="${Page} {{$index + 1}}" ng-attr-aria-current="{{($index + 1) == data.p ? 'page' : undefined}}">{{$index + 1}}</a>
        </div>
        <div class="btn-group">
          <a href="javascript:void(0)" ng-click="setPageNum(data.p + 1)" ng-class="{'disabled': data.p == data.num_pages}" class="btn btn-default" aria-label="${Next page} {{data.p == data.num_pages ? '${disabled}' : ''}}" tabindex="{{(data.p == data.num_pages) ? -1 : 0}}"><i class="fa fa-chevron-right"></i></a>
        </div>
      </div>
      <div class="m-t-xs panel-title">${Rows {{data.window_start + 1}} - {{ mathMin(data.window_end,data.row_count) }} of {{data.row_count}}}</div>

      <span class="clearfix"></span>
    </div>
  </div>

All in all, I had to insert code in four different places, twice in the table header, once for the table rows, and once for the bulk action drop-down in the table footer. Everything else is just as it was in the original HTML template from the latest version of the widget. This I like much better than the way that I had it before, and each block of code that I inserted is clearly marked with a Start and an End comment. I took the same approach with the server script and the client script, attempting wherever possible to leave as much as the original work in place and marking those sections where I inserted additions.

Now that that is done, I need to go back into the wrapper widgets and basically do the same thing after I remove all of the code that is now being handled in the core widget. Once that is done, there is still one more thing that I would like to do before releasing another Update Set, but I need to finish up those wrapper widgets first before we jump into that.

Aggregate List Columns, Part XI

“When obstacles arise, you change your direction to reach your goal; you do not change your decision to get there.”
Zig Ziglar

Last time, I completed the changes that I wanted to make to support the ability to click on an aggregate column value and see a list of the records represented by the value. I fully intended to test everything out this time and wrap this up; however, once I started testing things out, I began to realize that there were some inconsistencies in the approach taken for the aggregate columns compared to that taken with the buttons and icons. I don’t really like seeing that, so now I am contemplating scrapping the whole thing and starting over. I’m not fully there just yet, but I really don’t like have two different solutions to virtually the same objective.

To begin my testing, I built a simple modal pop-up widget and then edited the original AggregateTestConfig Script Include to contain an action property with a value of broadcast. That produced the desired clickable link on the aggregate column, but I noticed that there was no tool tip on mouse-over like there is with the buttons and icons. That, of course, is because I did not add the hint property to the aggregate column specification object like there is in the button/icon specification object. That seems like an easy fix, but the other thing that I did not like was that the column was still a link when the value was 0. Since there is nothing to see when the value is 0, it seems to me that that column shouldn’t be clickable unless there are records to view. That should also be an easy fix, so I set both of those concerns aside and moved on to testing the other option, linking to a new page.

Not wanting to disturb my earlier testing, I pulled up the AggregateTestConfig2 Script Include to use the second test page for this effort. You may recall that the second test page is a list of sys_user_grmember table records, not sys_user table records. To make this work for counting up the related records, we added the optional source property to the aggregate column specification object, but the code that I added to support the clickable links did not reference that property. That needed to be addressed as well, but I still wanted to do some testing, so I decided to swap over to the AggregateTestConfig3 Script Include to use the third existing test page instead of the second. That allowed me to complete my testing, and everything seemed to work as it should, but in digging around in the code, I found a couple of things that really disturbed my sense of The Way Things Ought To Be.

For one, to provide a link for buttons and icons, we use the property page_id. To accomplish the same thing for aggregate columns, the property name is action. The first is simply the ID of an existing Portal Page, selected from an sn-record-picker of Portal Pages. The second is a URL query string that includes the ID of the desired Portal Page as well as any other parameters that you might want to include in your URL. Both achieve the desired objective, but again, I do not like to see two different approaches to the same issue in the same component. It does work, but I don’t like it.

The other thing that I discovered is that the code that I added to handle the link to the new page was in the core SHN Data Table widget, while the similar code for the buttons and icons was located in the various wrapper widgets. I am sure that I copied all of that from the original click handler for the entire row, but I am not sure why it is in the wrapper widgets, where it has to be replicated in each and every one, instead of in the core widget, where it would seem to belong. Maybe there was a reason for that when the stock items were first constructed, but I do not know what that reason might have been.

In the midst of all of that, I came across this conversation, which included a link to this article. While I happen to share the article author’s concerns about cloning stock widgets, and he does make a number of valid points in his reply to the original poster, some modifications that you would like to make are so extensive that it makes little sense to attempt to retain whatever might be left of the original artifact. Still, his approach of embedding the original widget underneath your enhancements, which is essentially what the wrapper widgets do with the core data table widget, is an intriguing idea. I still have to study the sample to see how that might be adapted to what I have been trying to do, but I think it might be worth a closer look.

All of things these piled on top of one another make me think that maybe I want to back up the truck and take another shot at this from a different perspective. I definitely want to be able to click on a non-zero aggregate column and see a list of the records represented there, either in a modal pop-up on a new page, but maybe the way that I jumped into this was just a little too hasty. I think I have to do a little more digging around before I fully commit to what I have started here. Maybe this could be done a little better than I have it now. Or maybe not. Hopefully, I can figure all of that out relatively soon and we can still wrap this up and put it behind us.

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!

sn-record-picker Helper, Corrected (again!)

“The biggest room in the world is the room for improvement.”
Helmut Schmidt

The other day I was working on adding Aggregate List Columns to my SNH Data Table collection and I lifted some code from the sn-record-picker Helper that looked like it might be fairly close to what I needed at the time. Unfortunately, when I tested it all out, I came to realize that the code that I lifted contained an error, and a pretty serious one at that. The problem is with the filter used on all of the table field drop-downs, which is supposed to include all of fields on the selected table. As it turns out, it includes all of the fields on the primary table, but does not include all of the other fields from any other table in the table hierarchy. That is definitely not good!

Tables can be extended from other tables, and in cases such as the CMDB, there can be quite a number of tables in the extension hierarchy. For example, the cmdb_ci_win_server table extends the cmdb_ci_server table, which extends the cmdb_ci_computer table, which extends the cmdb_ci_hardware table, which extends the cmdb_ci table, which extends the cmdb table. That’s a lot of missing fields if you only include the fields from the primary table. Clearly, this needs to be addressed, and should have been addressed long ago.

In digging through the code, the problem appears to be in this function that creates the filter for all of the field pickers:

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

The current filter that is built turns out to be:

name=(name of selected table)

… when what it really needs to be is:

nameIN(list of all tables in the heirarchy)

That is simple enough to do, but then, the question becomes, where do we get a list of all of the tables in the hierarchy? As usual, the Now Platform has anticipated that someone might need that, and includes a way to get that very information. The solution to our query is the TableUtils object. That object includes a method called getTables() that will return just such a list. Unfortunately for us, this object is only available on the server side, and up until now, our widget did not need to have any server-side code. Well, things change.

Here is how we can use this object to get a Javascript Array of table names:

var tu = new TableUtils(input.table.value);
var tableList = tu.getTables();
data.tableList = j2js(tableList);

The object returned by the getTables() method is actually a Java object, so we have to use the j2js (Java to Javascript) function to turn it into an Array that we can use. Here is the entire new widget Server script with this functionality included:

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

With that now available on the server side, we need to go into our client-side function and have it call the server to get the list, and then use that list to rebuild the filter in a way that will include all of the fields on all of the tables involved. Here is the new buildFieldFilter function:

$scope.buildFieldFilter = function() {
	c.data.ready = false;
	c.data.fieldFilter = 'name=' + c.data.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';
	});
};

If there is not more than one table involved, then we can leave the filter with the straight equals condition, but if there is more than one table in the returned list, then we overlay that with the IN condition instead. Either way, we tack on the rest of the original filter, and we should be good to go.

Well, that’s all there is to that. Now I just need to pull all of the parts together for a new Update Set, and we should be good until we come across the next previously undetected problem!

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.