SNH Data Table Widgets on Share, Updated

“Baby steps count, as long as you are going forward. You add them all up, and one day you look back and you’ll be surprised at where you might get to.”
Chris Gardner

Recently, I came across some issues with the Content Selector Configuration Editor related to Scoped Applications, so I made a number of small corrections and put out a new Update Set for the editor. What I did not do, however, was to create a new Update Set for the SNH Data Table Widgets, which are also bundled with the very same editor. Yesterday, I finally got around to correcting that oversight, and now you should be able to find version 2.5 out on Share.

Version 2.5 is essentially the exact same bundle as the previous version (2.4.1), with the only change being the inclusion of the corrected configuration editor. Still, it does address the issues related to scoped configuration scripts, so it’s probably worth pulling down and installing it, just to avoid running into those annoying problems one day in the future. There are no new features or components in this new version, but it does now include the latest of everything, so this is the one that you will want.

Collaboration Store, Part LXXXII

“It’s really complex to make something simple.”
Jack Dorsey

Last time, we wrapped up an initial version of the storefront and released a new Update Set in the hopes that a few folks out there would pull it down and take it for a spin. While we wait patiently to hear back from anyone who might have been willing to download the new version and run it through its paces, let’s see if we can’t add a little more functionality to our shopping experience. The page that we laid out had a place for two widgets, but we only completed the primary display widget so far. The other, smaller portion of the screen was reserved for some kind of search/filter widget to help narrow down the results for installations where a large number of applications have been shared with the community. Adding that second widget to the page would give us something like this:

Storefront with new search/filter widget added

Rather than have the two widgets speak directly to one another, my thought was that we could use the same technique that was used with the Configurable Data Table Widget Content Selector bundled with the SNH Data Table Widgets. Instead of having one widget broadcast messages and the other widget listen for those messages, the Content Selector communicates with the Data Table widget via the URL. Whenever a selection is made, a URL is constructed from the selections, and then the Content Selector branches to that URL where both the Content Selector and the Data Table widget pull their control information from the shared URL query parameters. We can do exactly the same thing here for the same reasons.

For this initial attempt, I laid out a generic search box and a number of checkboxes for various states of applications on the local instance. To support that, we can use the following URL parameters: search, local, current, upgrade, and notins. In the widget, we can also use the same names for the variables used to store the information, and we can populate the variables from the URL. In fact, that turns out to be the entirety of the server-side code.

(function() {
	data.search = $sp.getParameter('search') ? decodeURIComponent($sp.getParameter('search')) : '';
	data.local = $sp.getParameter('local') == 'true';
	data.current = $sp.getParameter('current') == 'true';
	data.upgrade = $sp.getParameter('upgrade') == 'true';
	data.notins = $sp.getParameter('notins') == 'true';
})();

And here is the HTML that I put together to display the information.

<div class="panel">
  <div class="row">
    <div class="col">
      <p>&nbsp;</p>
    </div>
  </div>
  <div class="row">
    <div class="col">
      <input ng-model="c.data.search" name="search" id="search" type="text" class="form-control" placeholder="&#x1F50D;"/>
    </div>
  </div>
  <div class="row">
    <div class="col">
      <input ng-model="c.data.local" name="local" id="local" type="checkbox"/>
      <label for="local">${Local}</label>
    </div>
  </div>
  <div class="row">
    <div class="col">
      <input ng-model="c.data.current" name="current" id="current" type="checkbox"/>
      <label for="current">${Current}</label>
    </div>
  </div>
  <div class="row">
    <div class="col">
      <input ng-model="c.data.upgrade" name="upgrade" id="upgrade" type="checkbox"/>
      <label for="upgrade">${Upgrade Available}</label>
    </div>
  </div>
  <div class="row">
    <div class="col">
      <input ng-model="c.data.notins" name="new" id="new" type="checkbox"/>
      <label for="notins">${Not Installed}</label>
    </div>
  </div>
  <div class="row">
    <div class="col text-center">
      <button ng-click="search()" class="btn btn-primary" title="${Click to search the Collaboration Store}">${Search}</button>
    </div>
  </div>
</div>

To format things a little nicer, and to kick that little magnifying glass search icon over to the right, we also need to throw in some CSS.

.col {
  padding: .5vw;
}

label {
  margin-left: .5vw;
}

::placeholder {
  text-align: right;
}

There is one client-side function referenced in the HTML for the button click, and that’s pretty much all there is to the widget’s Client script.

api.controller = function($scope, $location) {
	var c = this;

	$scope.search = function() {
		var search = '?id=' + $location.search()['id'];
		if (c.data.search) {
			search += '&search=' + encodeURIComponent(c.data.search);
		}
		if (c.data.local) {
			search += '&local=true';
		}
		if (c.data.current) {
			search += '&current=true';
		}
		if (c.data.upgrade) {
			search += '&upgrade=true';
		}
		if (c.data.notins) {
			search += '&notins=true';
		}
		window.location.search = search;
	};
};

The search() function builds up a URL query string based on the operator’s input and then updates the current location with the new query string, essentially branching to the new location. When the new page loads, both the search widget and the storefront widget can then pull their information from the current URL. We can test all of this out now by saving the new widget, pulling up our collaboration_store page in the Service Portal Designer, and then dragging our new widget onto the page in the space already reserved for that purpose.

Dragging the new widget onto the existing page

With that completed, we can now try out the page and see that entering some data and clicking on the Search button actually does reload the page with a new URL. However, at this point the content of the primary page never changes because we have yet to add code to the main widget to pull in the URL parameters and then use that data to adjust the database query. That sounds like a good subject for our next installment.

Scripted Value Columns, Part VIII

“Too many men work on parts of things. Doing a job to completion satisfies me.”
Richard Proenneke

Last time, we wanted to wrap up the modifications on the last two wrapper widgets and put out a new Update Set, but we discovered that we missed an important element in our list of things that would need to be modified, the Configurable Data Table Widget Content Selector widget. We need to take a look at that guy and see what needs to be done to accommodate scripted value columns, and then retest the third wrapper widget, which shares the page with this component.

A quick scan of the Server script for aggarray comes up empty, but in the Client script, we come across this:

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

Making a copy of that and doing a little string replacement here and there gives us an equivalent block of code for the new scripted value column configurations.

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

And that seems to be all there is to that. Now we can go back to our last test and run it again to see if that fixed our problem.

Successful test of the third wrapper widget

That’s better! Now we have a column for the Last Comment, and we even have a row with some data in it. Good deal. And just to check on the content selector widget, we can look at the URL that it built to see how the configuration options for the scripted value columns appeared in the URL.

/sp?id=my_things&table=incident&filter=caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe^active%3Dtrue&fields=number,opened_by,opened_at,short_description&scripteds=[{"heading":"Last Comment","name":"last_comment","label":"Last Comment","script":"global.ScriptedJournalValueProvider"}]&aggregates=&buttons=&refpage={"sys_user":"user_profile"}&px=requester&sx=open&spa=1&p=1&o=opened_at&d=asc

Well, that’s the whole thing, but we can zoom in on the part in which we have an interest.

scripteds=[{"heading":"Last Comment","name":"last_comment","label":"Last Comment","script":"global.ScriptedJournalValueProvider"}]

So that is the last of the wrapper widgets, and unless we have left something else out, that’s the last of the work to be done to implement this new feature. Now all that is left is to bundle the whole thing up into a new Update Set and post it out on Share as a new version.

Here is the new Update Set, and here is where you can find it on Share. If you happen to use it, find it to be of value, or run into any issues, please let us all know in the comments below.

Scripted Value Columns, Part VII

“Unplanned occurrences are reminders to check your tendency to think that you’re the one in control.”
James Martin

Last time, we created another example of how one might utilize the new scripted value column feature, this time with catalog item variables instead of Incident journal entries. There are a number of other things that we could try, but two examples should be enough to get the point across, and I’ll leave it to others to come up with additional examples of their own.

We still have two more wrapper widgets to update, though, and we still have that annoying misalignment between the original columns and the new. Here is the way things come out right now:

Misalignment of original columns and new columns

… and here is the way that it should look:

Correct alignment of original columns and new columns

I was able to capture that second image because I found and fixed the problem. I had to replace this HTML:

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

… with this:

<span style="display: inline-flex;">
  <span style="display: inline-flex;" ng-if="item[field].value && item[field].type == 'reference' && item[field].table == 'sys_user'">
    <sn-avatar primary="item[field].value" class="avatar-small" show-presence="true" enable-context-menu="false"></sn-avatar>
    &nbsp;
  </span>
  <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>
</span>

Now, without getting into too much detail that no one really cares about, the source of the problem was the sn-avatar tag, which I added a while back so that user columns would have the avatar in front of the name. For some reason, the tag renders out a carriage return and a handful of spaces just before the avatar image. With the ng-if attribute set to false, this collection of white space is still rendered on the page, even when the avatar itself is not. I solved that problem by wrapping the avatar tag with a span and putting the ng-if attribute on the outer span rather than on the sn-avatar tag. That took care of things for columns where there was no avatar, but the user columns, which show the avatar, were still out of alignment with the rest of the columns. Adding style=”display: inline-flex; took care of that problem with the avatar, but then the user name ended up underneath the avatar instead of next to it. To solve that problem, I wrapped the whole thing in another span with the same style attribute. Now everything lines up the way that it should.

Now that that is out of the way, we still have two more wrapper widgets to update. Let’s jump into the SNH Data Table from Instance Definition and do the same kind of searching we did before, looking for some code that might need to be copied and modified. On this particular widget, such a search turns up nothing at all in either the Server script or the Client script, so the only thing that we really need to do is to add another entry to the Option schema for our new scripted value column specification.

{"hint":"A JSON object containing the specifications for scripted value columns",
"name":"scripteds",
"default_value":"",
"section":"Behavior",
"label":"Scripted Value Column Specifications (JSON)",
"type":"String"}

To test this, we can modify our scripted_value_test_2 page to use this widget instead of the SNH Data Table from JSON Configuration widget, and then transfer our configuration options from the Script Include to the widget options.

Widget configuration options for the modified wrapper widget

Now all we need to do is to save it and then run out to the Service Portal and take a quick peek.

Testing the modified SNH Data Table from JSON Configuration widget

So that all looks good. And much, much better now that the column data all lines up as it should! It’s nice to finally have that fixed. That takes care of wrapper widget #2. Now let’s take a look at that last one, the SNH Data Table from URL Definition widget. The only line that appears to require modification is this one:

copyParameters(data, ['aggregates', 'buttons', 'refpage', 'bulkactions']);

… which we can convert to this to pick up our new configuration option:

copyParameters(data, ['scripteds', 'aggregates', 'buttons', 'refpage', 'bulkactions']);

Now we need to test it, so we will need to find or create a page that use this widget. Let’s take a look at the ones that are already out there by checking out the Related List down at the bottom of the form.

Pages that use the SNH Data Table from URL Definition widget

The page my_things looks like a good candidate, so we can take a look at the configuration script that it uses and then edit it to add one or more scripted value columns. One of the tables utilized on that page is the Incident table, so let’s go ahead and use our existing value provider script to add a journal entry column to one of those.

name: 'incident',
displayName: 'Incident',
open: {
	filter: 'caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe^active=true',
	fields: 'number,opened_by,opened_at,short_description',
	svcarray: [{
		name: 'last_comment',
		label: 'Last Comment',
		heading: 'Last Comment',
		script: 'global.ScriptedJournalValueProvider'
	}],
	aggarray: [],
	btnarray: [],
	refmap: {
		sys_user: 'user_profile'
	},
	actarray: []
}

Now let’s take a look.

First test of the modified SNH Data Table from URL Definition widget

Well, that didn’t work! It’s always something. Even if there were no comments on any of these Incidents, we should still have a column heading for our new scripted value column. I don’t think this problem is in the widget that we just modified, however. This widget shares the page with the Configurable Data Table Widget Content Selector widget, and that is a widget that we have not even touched. That is going to have to be modified to accommodate our new feature as well, as it builds the URL that the SNH Data Table from URL Definition widget turns to for its configuration information. This was not on our list of things to do for this feature, but it definitely needs to be done.

I was hoping to wrap things up with this installment, but now we have a new widget to modify and more testing to do, so I think we will just save all of that, plus the Update Set creation, for our next time out.

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.