Yesterday, we made a bit of progress on my Generic Feedback Widget concept, but we left with quite a few loose ends that needed tidying up. I wasn’t too happy with the way that the date fields came out, so I went sniffing around for an out-of-the-box date formatter and came across the GlideTimeAgo object. This cool little doodad will not only format the date and time, but for relatively recent values, will turn it into a text describing just how long ago it was. To put it to use, I adapted a function that I discovered on another page and threw it down into the bottom of the widget’s server side code.
function getTimeAgo(glidedatetime) {
var response = '';
if (glidedatetime) {
var timeago = new GlideTimeAgo();
response = timeago.format(glidedatetime);
}
return response;
}
Then I updated the code that pulled the value out of the record.
Now recent comments will have timestamps such as 7 minutes ago or Just Now or 4 days ago. I like that much better.
Another issue was the need to translate live_profile sys_ids into sys_user sys_ids. I actually found that I needed to translate from one to the other in both directions, so I set up a couple of maps to retain anything that I found (so I wouldn’t have to look it up again if I needed to translate it again or go back the other direction). I ended up creating four different functions related to this process, one to pull from the map and another to populate the map for each of the two different ways this could be approached (live_profile to sys_user and sys_user to live_profile).
function getUserSysId(liveProfileId) {
if (!data.userSysIdMap) {
data.userSysIdMap = {};
}
if (!data.userSysIdMap[liveProfileId]) {
fetchUserSysId(liveProfileId);
}
return data.userSysIdMap[liveProfileId];
}
function fetchUserSysId(liveProfileId) {
if (!data.profileSysIdMap) {
data.profileSysIdMap = {};
}
var lp = new GlideRecord('live_profile');
if (lp.get(liveProfileId)) {
var userSysId = lp.getValue('document');
data.userSysIdMap[liveProfileId] = userSysId;
data.profileSysIdMap[userSysId] = liveProfileId;
}
}
function getProfileSysId(userSysId) {
if (!data.profileSysIdMap) {
data.profileSysIdMap = {};
}
if (!data.profileSysIdMap[userSysId]) {
fetchProfileSysId(userSysId);
}
return data.profileSysIdMap[userSysId];
}
function fetchProfileSysId(userSysId) {
if (!data.userSysIdMap) {
data.userSysIdMap = {};
}
var lp = new GlideRecord('live_profile');
lp.addQuery('document', userSysId);
lp.query();
if (lp.next()) {
var liveProfileId = lp.getValue('sys_id');
data.userSysIdMap[liveProfileId] = userSysId;
data.profileSysIdMap[userSysId] = liveProfileId;
}
}
To fix the sys_id references in the comment author link, I leveraged one of the above functions to swap out the live_profile id with the one that I needed from the sys_user table.
I also utilized those functions formatting the @mentions that can be present in live_message text. I wanted those to be links to the User Profile page as well, so I hunted down some existing code that formats @mentions on the Live Feed page and adapted it for my own purpose.
function formatMentions(text) {
if (!text) {
text = '';
}
var regexMentionParts = /[\w\d\s/\']+/gi;
text = text.replace(/@\[[\w\d\s]+:[\w\d\s/\']+\]/gi, function (mention) {
var response = mention;
var mentionParts = mention.match(regexMentionParts);
if (mentionParts.length === 2) {
var liveProfileId = mentionParts[0];
var name = mentionParts[1];
response = '<a href="?id=user_profile&table=sys_user&sys_id=';
response += getUserSysId(liveProfileId);
response += '">@';
response += name;
response += '</a>';
}
return response;
});
return text.replace('\n', '<br/>');
}
Most of that is regex gobbledygook that I couldn’t possibly understand, but it does work, so now any @mentions in the text of message are converted into links to the User Profile page of the person mentioned. As with the other corrections, I just needed to modify the code that populates the feedback object to make use of this new function.
Well, we didn’t get to the part where we take in new feedback, so we will have to take that up next time out. Still, we did make quite a few improvements in the way that the existing feedback is formatted, so we are making progress.
“Feedback is the breakfast of champions.” — Ken Blanchard
The other day when I was testing out my Static Monthly Calendar I needed a page to demonstrate the modal pop-up, so I grabbed a page that I had created to show off my feedback form field. That got me to thinking that I would be nice to have a universal widget to not only collect new feedback on an item, but to also display all of the feedback that had been collected so far. This is something that ServiceNow already does quite well for Knowledge documents, but that isn’t really universal, and can’t be applied to any other component. Still, it’s a nice model, and one that I wanted to emulate in a way that could be applied to other things.
Previous comments are listed in reverse chronological order followed by an input form where you can enter new comments. This would be the layout of my proposed widget, with the intent that you could use it for any type of entity, and not just Knowledge. I looked at the underlying table structure for this feature, and the data is eventually stored in two places, a Knowledge table called kb_feedback, and the Live Feed table, live_message. After looking into both, I decided that I could accomplish what I had in mind with the live_message table alone, so I cracked open a shiny new widget that I called generic_feedback and got to work.
I like to solve one problem at a time and then move on to the next one, so I started out with the basic structure of the HTML for the widget, basically copying the original from the Knowledge page:
My only significant deviation from the original was to make the name of the person leaving the comment a link to that person’s User profile page. Other than that, it pretty much followed the original layout.
Based on my data references, I was going to need an array of objects with userSysId, userName, dateTime, and comment properties. These values would all come from the live_message table, but first I needed to figure out how to select the specific messages for the particular item that was the subject of the page. One of the properties of the live_message table is labeled Conversation, and is a reference to another table, live_group_profile. The live_group_profile table contains two properties, table and document, that we can leverage to point to a specific record on a specific table, linking our feedback to pretty much anything in the ServiceNow database. Using this approach, to find all of the feedback for a particular entity, you need the name of the table and the sys_id of the record, and with that information, you can find the live_group_profile record that identifies the “conversation” about that entity. On the server side, that code looks something like this:
var conv = new GlideRecord('live_group_profile');
conv.addQuery('table', data.table);
conv.addQuery('document', data.sys_id);
conv.query();
if (conv.next()) {
data.convId = conv.getValue('sys_id');
var fb = new GlideRecord('live_message');
fb.addQuery('group', data.convId);
fb.orderByDesc('sys_created_on');
fb.query();
while(fb.next()) {
var feedback = {};
feedback.userSysId = fb.getValue('profile');
feedback.userName = fb.getDisplayValue('profile');
feedback.dateTime = fb.getValue('sys_created_on');
feedback.comment = fb.getDisplayValue('message');
data.feedback.push(feedback);
}
}
Before we can gather up all of the feedback, though, we have to establish the name of the table and the sys_id of the record. For the purpose of this particular widget, we will require that this information be passed in the form of URL parameters. These are the very same URL parameters that drive the Dynamic Service Portal Breadcrumbs (which I also included on the page), and are pretty much a standard in the Service Portal, so that shouldn’t be too onerous of a requirement. The code to pull those values in from the URL and validate them looks like this:
if (input) {
data.table = input.table;
data.sys_id = input.sys_id;
} else {
data.table = $sp.getParameter('table');
data.sys_id = $sp.getParameter('sys_id');
}
if (data.table && data.sys_id) {
var gr = new GlideRecord(data.table);
if (gr.isValid()) {
if (gr.get(data.sys_id)) {
data.tableLabel = gr.getLabel();
data.recordLabel = gr.getDisplayValue();
...
}
}
}
This doesn’t give us a way to enter new feedback, but we have enough now to give things a try, so let’s create a page, drag our new widget onto the page, and see how things are looking so far.
Well, overall it didn’t turn out too bad, but I can see right away a number of things that need a little work. For one, I don’t really like the date format. That definitely needs some improvement. Also, my links don’t work for the User Profile page. That’s because the profile column in the live_message table contains the sys_id of the live_profile record, not the sys_user record. That’s not going to work. It’s always something!
Still, I like the way things are shaping up. Next time out, let’s see if we can’t clean up all of those pesky little issues and then work on capturing new comments and posting them back to the database.
“Do not wait; the time will never be ‘just right.’ Start where you stand, and work with whatever tools you may have at your command, and better tools will be found as you go along.” — George Herbert
Recently, I needed to display a monthly calendar with a few days singled out as noteworthy. That got me to thinking, as it usually does, that it would be nice to have the empty shell calendar as a separate component so that it could be re-utilized for other potential purposes. Aside from my own unique requirements, I could see where someone might want to have something like a payroll calendar filled with holidays and paydays or an event calendar with the line-up of live music each evening. Maybe you just want to call out National Step in a Puddle and Splash Your Friends Day or you just want to be able look at a calendar and see when the cafeteria is serving chipped beef on toast with steamed carrots, Tator Tots, and a butterscotch pudding cup. I have no idea what someone might want to do with a configurable calendar, but it seems like the possibilities could be endless, so it might be worth having a reusable part.
And maybe such a part already exists. ServiceNow actually has a built-in calendar that is used quite extensively, so that seemed like a good place to start. Like many ServiceNow components, it is its own Open Source project called, appropriately enough, Full Calendar. It is quite full, indeed, and has a lot of very cool features, but I was looking for something basic and read-only, and for my purposes, I was gong to have to figure out how to turn most of those features off. I just wanted a static, single-month view that was uneditable and not interactive in any way, except for maybe the ability to click on a date and pull up a little more information. I played around with it for a while, but in the end, I decided that it was going to be more work to turn off a bunch of capabilities than it would be to start off with something much more simplistic.
So then I scoured the Interwebs for a basic HTML calendar template, of which there are many, and finally settled on this one:
It pretty much had all of the characteristics that I was looking for, so I cracked open a brand new widget and pasted in the HTML and the CSS. Then I created a new Portal Page and dragged my new widget into a full-width container and pulled it up to see how it looked. Since it is just a template, everything is hard-coded, but at this point, I just wanted to make sure that I had all of the parts and pieces and that it came out looking as it should (which it did). This is just the parts that I had found at this stage, pasted into a new Service Portal Widget.
OK, so far, so good. This was the basic structure that I wanted; now, I just needed to replace the hard-coded data with something a little more dynamic. To start with, I wanted to be able to navigate to the next month and to the previous month. Nothing beyond that — just a simple forward/backward capability that would let you move around a bit. So I tinkered with the HTML for the header and came up with this:
This simply divided the header into three equal parts, the “go back” button, the original title (based on a variable now), and the “go forward” button. I had both buttons call the same routine, passing either a +1 or a -1, depending on which direction you wanted to go. That routine lives in the client script, which now looks like this:
function($scope, $location, spAriaFocusManager) {
var c = this;
$scope.newMonth = function(offset) {
var yy = parseInt(c.data.year);
var mm = parseInt(c.data.month) + offset;
if (mm < 0) {
mm = 11;
yy = yy - 1;
} else if (mm > 11) {
mm = 0;
yy = yy + 1;
}
var s = $location.search();
s.month = mm;
s.year = yy;
var newURL = $location.search(s);
spAriaFocusManager.navigateToLink(newURL.url());
}
}
The routine simply navigates to the same page with different values for the year and month URL parameters. To process those parameters when the page loads, we also need a little code on the server side:
This code basically establishes the values of the month and year based on the current date, and then if there is both a month and a year parameter on the URL, it overrides that initial value with the values passed via the URL. Then, using an array of month names, it establishes the calendar heading label based on the month and year values. At this point, we still have a hard-coded calendar body, but we can now use the new buttons to move forward and back through time, and the heading will change, even if the calendar itself still remains constant. All of that appears to work at this point, so now we just have to make the calendar itself as dynamic as the heading value.
To make the HTML portion simpler, I decided to build out the structure of the days to be displayed in the server side script. The structure is simply an array of 4 to 6 weeks, with each week containing an array of 7 days. The number of weeks in a month is variable, depending on how many 7-day rows it will take to display all of the days in the month, but the number of days in a week is always 7, regardless of how many of those days actually fall in the month to be displayed. The code to build out the model array is fairly self-explanatory, so I won’t dwell on that here, but here it is, for those of you interested in the details:
data.week = [];
data.firstDay = new Date(data.year, data.month, 1);
var offset = data.firstDay.getDay();
data.thisDay = new Date(data.firstDay.getTime());
while (data.thisDay.getMonth() == data.month) {
var thisWeek = [];
data.week.push(thisWeek);
for (var i=0; i<7; i++) {
var thisDay = {};
thisWeek.push(thisDay);
if (data.week.length > 1 || i >= offset) {
if (data.thisDay.getMonth() == data.month) {
thisDay.date = data.thisDay.getDate();
data.thisDay.setDate(data.thisDay.getDate() + 1);
}
}
}
}
Building the model in the server-side code makes the HTML portion quite simple. I deleted all of the hard-coded example code (except for the day of the week labels) and then replaced it with this:
Basically, it is just a couple of ng-repeats, one for the array of weeks and another within that for the 7 days of the week. The original calendar template had numbers for all of the days, whether they were in the current month or not, but I didn’t really like that approach, so I did not calculate those values, and threw in an ng-if to keep that number DIV from appearing for those days that do not belong to the month that is the subject of the current display. At this point, we now have a completely empty calendar based on the selected month and year:
This is pretty much the empty calendar shell that I had envisioned, although at this point there is no way to pass in content for any given day. I still have to work out the best way to go about that, but conceptually, this is exactly the kind of thing that I was hoping to produce: a plain monthly calendar with moderate navigation capabilities and the potential for adding custom content based on the use case. This looks like a good stopping place for now, so I have bundled up the parts thus far and created an Update Set for anyone who might have an interest. Next time, I will add the capability to pass in content, and come up with some kind of example to demonstrate how that all works.
“If you don’t have time to do it right, when will you have the time to do it over?” — John Wooden
After adding a mention type to my Service Portal form field tag, I broke my original ment.io example. I actually used that example in building the code to support the mentions type, but I did have to make a few changes in order to get everything to work, and now that I have done that, my original example doesn’t work anymore. I thought about just abandoning it since I got the form field version working, but then I thought that there were some other possible use cases where I wouldn’t want to be using an snh-form-field, so I decided that I had better dig around and see what was what.
The form field tag version did make things quite a bit simpler. Compare the initial version:
<snh-form-field
snh-model="c.data.textarea"
snh-name="textarea"
snh-type="textarea"
snh-label="${Original ment.io Example}"
snh-help="While entering your text, type @username anywhere in the message. As you type, an auto-suggest list appears with names and pictures of users that match your entries. For example, if you type @t, the auto-suggest list shows the pictures and names of all users with names that start with T. Click the user you want to add and that user's name is inserted into the @mention in the body of your text."
mentio=""
mentio-macros="macros"
mentio-trigger-char="'@'"
mentio-items="members"
mentio-search="searchMembersAsync(term)"
mentio-template-url="/at-mentions.tpl"
mentio-select="selectAtMention('textarea', item)"
mentio-typed-term="typedTerm"
mentio-id="'textarea'"
ng-trim="false"
autocomplete="off"/>
… with the snh-form-field version, and you can see right away that there is a significant difference in size alone:
Just for comparison, I decided to put both on the page (and make them both work!), and then add another using the new feedback field type, and yet another using just a plain, old, ordinary textarea.
The errors on the original weren’t that significant. Mainly I just wanted to put out a new version of the Update Set so that you could see all of the various approaches working on the same page side by side (… well, on top of each other, if you really want to get specific). If you are really curious as to what exactly needed to be changed, you can always compare the old to the new.
“Have a bias toward action – let’s see something happen now. You can break that big plan into small steps and take the first step right away.” — Indira Gandhi
After working out all of the little issues in my @mentions example, it occurred to me that it might be even better to just make an @mentions textarea another supported field type like I just did for feedback. This way, you wouldn’t have to bother with all of thosementio- prefixed attributes at all — they would just become part of the internal process of rendering the form field. Some things would have to be standardized, which might limit your flexibility in certain areas, but that still wouldn’t prevent you from mentionizing your own textarea or textarea-type form field. This would just be yet another option.
The first thing to, then, would be to add mention to the list of supported field types:
That’s getting to be quite a list, but that’s not necessarily a bad thing. Next, we have to add the logic to render the input element:
} else if (type == 'mention') {
htmlText += " <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" mentio=\"\" mentio-macros=\"macros\" mentio-trigger-char=\"'@'\" mentio-items=\"members\" mentio-search=\"searchMembersAsync(term)\" mentio-template-url=\"/at-mentions.tpl\" mentio-select=\"selectAtMention('" + name + "', item)\" mentio-typed-term=\"typedTerm\" mentio-id=\"'" + name + "'\" ng-trim=\"false\" autocomplete=\"off\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
So far, so good. Now, where to stuff the template? In my mind, the best place to locate the template would be in the Service Portal itself, as that would ensure that it would only appear on the page once and only once. You could just make it part of your portal page header and be done with it. However, if you didn’t know to do that, or if you moved your widget from a portal that had it to another portal that did not, things would not work and would not be that intuitive as to why that was. Placing it in the widget is another option, but again, you would have to know to do that. Rendering it as part of the form field means that you could have many copies of the template ending up on a single page, but then everything would be self-contained and you wouldn’t have to know to add anything else to include this feature other than the appropriate form field type. Right now, I am thinking that this latter approach is the best option, although I may end up going the other way one day after I’ve had more time to contemplate all of the ramifications of that strategy. For now, I’ve added this line just under the lines above:
That does create the potential for more than one template appearing in the source code, but other than the obvious bloat, that doesn’t really hurt anything.
Now we have to deal with the functions, one to fetch the data and one to handle the selection. We will add these to the link section of the provider, starting with the one that fetches the data:
This script required two things that we brought in via the client script function arguments: snMention and $timeout. In this context, $timeout can be replaced with the standard window functions setTimeout and clearTimeout, so that eliminates the need for that one. The other requirement, though, snMention, is still going to have to come in via a client script function argument, and then passed into the scope. I don’t like doing that, as that is yet another thing that the person using this is going to have to know, but I couldn’t see any other way around it. That makes the client script in our example widget now start out like this:
function($scope, snMention) {
var c = this;
$scope.snMention = snMention;
The other script we will need is the one that handles the selection:
The major revision here is that we have added a field argument to the function call to allow for the fact that there could be more than one field on the form that contains @mentions. This way, mentions from one field are collected in a different object than mentions from another field. We also added checks for both the base mention map and the field-specific map so that they can be initialized as needed.
To test things out, I added a mention type form field to my form field test widget and gave it a whirl.
Everything seems to work, which is good, so I’ve bundled it all up into yet another Update Set just in case someone wants to dig into all of the details. I’m going to have to quit using the word final in relationship to this little form field experiment, as I keep finding new form field types to toss onto the pile; who knows what will pop up tomorrow …
There is a nifty feature on the UI side of the Now Platform that allows you to include “mentions” of other platform users in various messages and form fields such as the Comments and Work Notes on the Incident form. To mention another user, you simply type an @ character before typing out their name:
I stole that image from the documentation, which you can find here. It’s a nice feature, and it opens up a number of possibilities for other cool stuff, but it’s only available on the primary UI side. As of yet, this feature has not found its way to the Service Portal. I’m sure that if I were to wait long enough, some future version will resolve that minor shortcoming, but not being one who likes to wait for things, I though that maybe I would try to see if I could make it work myself. How hard could it be?
I started out by digging around in the source code of the Incident form, trying to figure out how everything worked. I located the textarea tag for the comments field and took a look at all of the attributes:
Clearly, everything that started with mentio was related to this feature, so on a whim I decided to search the Interwebs for the term mentio and discovered that the good folks at ServiceNow were using this product to implement this feature. That was actually a nice find, as the site came complete with documentation, which really made figuring all of this out quite a bit easier than I had originally imagined.
I wasn’t sure how many of the parts and pieces needed to make this work were already present in the Service Portal, so my first attempt was just to create a widget with a single textarea form field and add all of these mentio– attributes:
<snh-form-field
snh-model="c.data.textarea"
snh-name="textarea"
snh-type="textarea"
snh-label="${ment.io Example}"
snh-help="While entering your text, type @username anywhere in the message. As you type, an auto-suggest list appears with names and pictures of users that match your entries. For example, if you type @t, the auto-suggest list shows the pictures and names of all users with names that start with T. Click the user you want to add and that user's name is inserted into the @mention in the body of your text."
mentio=""
mentio-macros="macros"
mentio-trigger-char="'@'"
mentio-items="members"
mentio-search="searchMembersAsync(term)"
mentio-template-url="/at-mentions.tpl"
mentio-select="selectAtMention(item)"
mentio-typed-term="typedTerm"
mentio-id="'textarea'"
ng-trim="false"
autocomplete="off"/>
That at least got me the basic structure with which to experiment, and turned out looking like this:
Of course, typing an @ character did not immediately pop up the expected user selection screen, but I really didn’t expect that at this stage of the game. Three of those mentio attributes in particular seemed to suggest that there was more parts needed to make all of this work:
The first and the last I recognized as missing Javascript functions, but I wasn’t sure what that one in the middle could be. A quick check of the ment.io documentation revealed this:
mentio-template-url Optional. Specifies the template url to use to render the select menu. The template should iterate the items list to present a menu of choices. The items scope property from the mentio-menu is available to iterate within an ng-repeat. The typedTerm scope property from the mentio-menu can be accessed in order to highlight text in the menu. The default template presents a simple menu, and assumes that each object has a property called label.
Armed with that little tidbit of additional knowledge, I went back to the source code of the Incident form, and found this:
<script type="text/ng-template" id="/at-mentions.tpl">
<div class="dropdown-menu sn-widget sn-mention">
<ul class="sn-widget-list_v2">
<li ng-if="items.length > 0 && !items[0].termLengthIsZero" mentio-menu-item="person" ng-repeat="person in items">
<div class="sn-widget-list-content sn-widget-list-content_static">
<sn-avatar primary="person" class="avatar-small" show-presence="true"></sn-avatar></div>
<div class="sn-widget-list-content">
<span class="sn-widget-list-title" ng-bind-html="person.name"></span>
<span class="sn-widget-list-subtitle" ng-if="!person.record_is_visible">Cannot see record</span></div></li>
<li ng-if="items.length === 1 && items[0].termLengthIsZero">
<div class="sn-widget-list-content">
<span class="sn-widget-list-title sn-widget-list-title_wrap">Enter the name of a person you want to mention</span></div></li>
<li ng-if="items.length === 0 && items.loading && visible">
<div class="sn-widget-list-content sn-widget-list-content_static">
<span class="sn-widget-list-icon icon-loading"></span></div>
<div class="sn-widget-list-content">
<span class="sn-widget-list-title">Loading...</span></div></li>
<li ng-if="items.length === 0 && !items.loading">
<div class="sn-widget-list-content">
<span class="sn-widget-list-title">No users found</span></div></li></ul>
</div>
</script>
I’ve never seen HTML stored inside of a script tag before, but obviously it works, so I just copied that into the HTML for the widget. Then I hunted down the two missing functions that were referenced in the other attributes, pasted those into the widget’s client script and gave it another try. Still nothing. Now it was time to take a hard look at those functions and see if I couldn’t toss in some alerts here and there to see if I could figure out what was working and what was having issues. I figured that the best place to start was with the function that fetched the data to be display on the pick list. Here is the original script from the Incident page:
That’s not very nicely formatted, so it’s a little hard for me to read, but I noticed two things: 1) it was using a component called snMention to fetch the data, and 2) it was passing the function a table and a sys_id in addition to what the person had typed on the screen. The first thing that I did was add snMention as an argument to the client script function along with $timeout, which was also referenced in the script. I also wasn’t on a form associated with a table, so I replaced those two variables with null. That allowed the function to work, but it did not send back any data. Apparently, you have to send in the parameters for a record to which the current user has write authority. I didn’t have one of those, so I decided to try the user’s sys_user record, which actually did the trick. I still wasn’t getting anything on the screen, but at least I could see that I was actually getting results based on what was being entered in the field. That was progress. Here is how the client script turned out after all of that:
After a lot of trial and error (mostly error!), I finally figured out that the reason that I wasn’t getting anything to show up on the screen was that I was missing some really important CSS. Rather than pick through the style sheets and pull out the various bits that I needed, I just added the following to the top of my HTML:
That was a little lazy, as I am sure that I only needed a few odds and ends from those additional style sheets, but this is just an example, so I just pulled in the entire thing, unnecessary bloat and all. Once I got everything working as it should, I wanted to do something with the list of folks who were mentioned in the text, just to show how that part works as well, so I created another widget to pop up in a modal dialog that listed out the people. Now, when you hit the Submit button on this little example, you get this:
In addition to the separate widget to display the names, I had to add one more function to the client side script of the main widget:
$scope.showMentions = function() {
var mentions = [];
for (var name in $scope.mentionMap) {
mentions.push({name: name, sys_id: $scope.mentionMap[name]});
}
spModal.open({
title: 'Users Mentioned',
widget: 'mentions',
widgetInput: {mentions: JSON.stringify(mentions)},
buttons: [
{label: 'Close', primary: true}
],
size: 'lg'
});
}
There is still quite a bit of clean-up that I would like to do before considering this really ready for prime time, but I achieved my main objective, which was just to see if I could get it to work. It does work, and if I don’t say so myself, it’s a pretty cool feature to add to the toolbox. If you would like to play around with code yourself, here is an Update Set with everything that you will need to pull this off.
“Code reuse is the Holy Grail of Software Engineering.” — Douglas Crockford
One of the thing that I like about making parts is that, even after you’ve “completed” your work and put a part on the shelf, you can always go back at some point later on and make it even better. When I first set out to create my form field tag, my primary goal was to save myself some work and to set things up so that things would always come out consistently. Consistency is a nice by-product of reusing the same component over and over again. People like it when things are consistent. So when I came across a need for a field type that I had not built into the current version of my form field tag, my first impulse was to pull it off the shelf, dust it off, and give it a bit of an upgrade.
What I needed was a feedback field, which is really a combination to two separate fields, a rating widget and a comments box. There are all kinds of rating widgets out there where you can configure stars or happy faces or some other graphic to indicate some level of satisfaction, but none of the existing field types in my current form field implementation supported that feature, and none of them included a single label for two input elements. What I wanted to do was to support something like this:
For the rating, I peeked under the hood of the Knowledge Article widget, and found the uib-rating tag, which looks like it comes from here. That looked like just the thing that I needed, so I all that was left for me to do was to wrap my labels and decorations around that widget in the same manner as I had for the sn-record-picker and sn-choice-list tags. The code for the stand-alone rating was pretty much just a copy, paste, and slightly modify:
if (type == 'radio' || type == 'inlineradio') {
htmlText += buildRadioTypes(attrs, name, model, required, type);
} else if (type == 'select') {
htmlText += buildSelect(attrs, name, model, required);
} else if (SPECIAL_TYPE[type]) {
htmlText += buildSpecialTypes(attrs, name, model, required, type, fullName, label);
} else if (type == 'reference') {
htmlText += " <sn-record-picker field=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-record-picker>\n";
} else if (type == 'choicelist') {
htmlText += " <sn-choice-list sn-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-choice-list>\n";
} else if (type == 'rating') {
htmlText += " <uib-rating ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></uib-rating>\n";
} else ...
For the combination of a rating and a comment block, things got a little more complicated. In all of my other field types, I passed through to the input element all of the original attributes that did not have some other purpose in rendering out the entire block of code. For the first time, I had more than one input element, as I was combining the rating doodad with the textarea for the comments all under one label. After experimenting with different ways to distinguish attributes for one of the elements from attributes for the other, I decided against making things more complicated than they needed to be, and just assume that all non-standard attributes would be attributed to the textarea. Once that was settled, the resulting code turned to be the following:
...
} else if (type == 'rating') {
htmlText += " <uib-rating ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></uib-rating>\n";
} else if (type == 'textarea' || type == 'feedback') {
if (type == 'feedback' && attrs.snhRatingModel) {
var max = 5;
if (attrs.snhRatingMax && parseInt(attrs.snhRatingMax) > 0) {
max = parseInt(attrs.snhRatingMax);
}
htmlText += " <uib-rating ng-model=\"" + attrs.snhRatingModel + "\" max=\"" + max + "\"/>\n";
}
htmlText += " <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
} else {
htmlText += " <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
}
As you can see, I did break down and allow for the max attribute by creating an snh-rating-max attribute that would not be passed in to the textarea, but other than that, the rating paired with the comments ends up basically unconfigurable. I may change that one day, but for now, this was all I needed to get me what I was after.
Anyway, I have done a little testing, and it all seems to work so far. If you would like to play around with it on your own, here is an Update Set with all of the relevant parts and pieces.
I love making parts. One of the reasons that ServiceNow is such a powerful platform is that it is built on reusable parts, and the platform encourages the development and use of reusable parts. I have known a number of developers who are extraordinary coders that can lay down reliable code at an amazing rate of speed, but no one can code faster than they can pull an existing part down from a shelf. When you build a part in such a way that you can reuse it again in another context, you not only solve your current problem, but you also create the solution for problems that you haven’t even encountered yet. That doesn’t just improve productivity, it also increases reliability. Parts on the shelf are on the shelf because you have used them somewhere before, and if you’ve used them before, then you’ve tested them before, which means that you’ve already gone through the exercise of shaking out all of those initial bugs. Faster and better — it’s a win/win.
When I started my Highcharts experiment, my goal was to create a reusable component for displaying any Highcharts graph. I was able to do that with my Generic Chart widget, but in the process, I also ended up showcasing a number of the other little parts and pieces that have been developing on the Now Platform. For example, when I wanted to add the ability to click on the chart and drill down to the underlying data, I ended up linking to my enhanced Data Table widget. I actually built that to support my configurable content selector, but once created (and tested) and placed on the shelf, it was there to later pull down, dust off, and utilize for other, previously unforeseen purposes.
To make the various selections for the four different parameters used in the workload chart, I ended up using the angular provider that I put together to produce form fields on the Service Portal. I didn’t create that to support the workload chart, but once it was created, there is was on the shelf to pull down and put to use.
I built my dynamic Server Portal breadcrumbs because I really didn’t like the way that the out-of-the-box breadcrumbs widget required you to pre-define the trail of pages displayed by the widget. That philosophy assumes that each of your pages can only be reached through a single path. For someone who likes to build and leverage reusable parts, this just seemed a little too restrictive to me. This was yet another “part” that I had build with my content selector in mind, but which was equally useful once I started linking out from my workload chart. In fact, it was while working with my workload chart that I discovered a flaw in my breadcrumbs widget. That’s another nice thing about working with reusable parts: when you fix a problem, you don’t just fix it for your purposes; you fix it for everyone else who is using it for whatever other purpose. That’s the difference between cloning and reusing. If you clone a part and find a flaw, you fix your copy, but the original and any other clones are unaffected. If you reuse a part and fix a flaw, you fix it for everyone.
“Never give up on something that you can’t go a day without thinking about.” — Winston Churchill
Those of us who develop software for a living always like to blame the customer for the inevitable scope creep
that works its way into an assignment or project. The real truth of the
matter, though, is that a lot of that comes right from the developers
themselves. Often, just when you think you are about to wrap something
up and call it complete, you get that nagging you-know-it-would-be-even-better-if-we-added-this feeling that just won’t go away.
It was my intention to make my previous installment on the Highcharts Workload Chart my last and final offering on the subject. Wait … this sounds way too familiar. OK, fine … I have a bad habit of continuing to tinker with stuff long after it should have been put to bed. But, I did learn something new this time, so I think it was worth the return trip. All I really wanted to do was to add a couple more relevant charts to my workload status page:
I already had the Generic Chart widget in hand, so I just needed to drop it on the page a couple more times and run a few more queries to fetch the relevant data. How hard could it be? Well, experience gives us the answer to that question! As usual, things didn’t go quite as smoothly as I had anticipated. It turns out that the Generic Chart widgetcontains a fatal flaw that only allows it to be used once per page. In the HTML for the widget, I hard-coded the ID of the DIV that will contain the chart, which you have to pass to Highcharts so that the chart will be rendered in that specific DIV. Well, when you put more than one Generic Chart widget on a page, they all want to render their charts in the first instance of a DIV with that ID. That’s not going to work!
The fix wasn’t too bad, but it did require a fix. The revised HTML now looks like this:
To populate the new data.container variable, I set up yet another widget option, and then set up a default value for the option so that you would only need to mess with this if you were working on a multi-chart page. That, and the addition of the code to populate the two other charts, one a Pie Chart and one a Bar Chart, were really the only other additions. If you want to take a look at the whole thing in action, here is the most recent Update Set.
“Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.” — Linus Torvalds
While I was playing around with my workload chart, I noticed a little bug in my dynamic breadcrumbs widget: returning to the Home page does not reset the breadcrumbs for a new trail out from the home page. Instead, all of the previous trail of pages remains intact. This is not the way that this is supposed to work. Once you return to the home page, the trail should start over. At first, I never noticed this, because I never added the breadcrumbs widget to the home page. Without the breadcrumbs widget on the home page, I wouldn’t expect it to reset. But once I did that and it still didn’t work, I had to get busy trying to figure out why that was.
The breadcrumbs data that was left behind on the previous page is retrieved from the User Preference on the server side, and then the new breadcrumbs data is established on the client side. Initially, the breadcrumbs data is set to an empty Array, and then if we are not on the home page, we push all of the existing pages onto the array until we come across the current page or run out of existing pages. In the case of the home page, the empty array is all that remains. Here is the relevant code:
c.breadcrumbs = [];
var thisPage = {url: $location.url(), id: $location.search()['id'], label: c.data.page || document.title};
if (thisPage.id != $rootScope.portal.homepage_dv) {
var pageFound = false;
for (var i=0;i<c.data.breadcrumbs.length && !pageFound; i++) {
if (c.data.breadcrumbs[i].id == thisPage.id) {
c.breadcrumbs.push(thisPage);
pageFound = true;
} else {
c.breadcrumbs.push(c.data.breadcrumbs[i]);
}
}
if (!pageFound) {
c.breadcrumbs.push(thisPage);
}
}
c.data.breadcrumbs = c.breadcrumbs;
c.server.update();
On the server side of things, my intent was to save the breadcrumbs once established, and here is the code that was supposed to handle that:
if (input.breadcrumbs) {
gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
}
My thinking was that, if the breadcrumbs were established on the client side, then save them to the User Preference. Unfortunately, the simple conditional input.breadcrumbs returns false for an empty array, not true as I had assumed (hey, an empty array is still something and not null!); therefore, the saving of the breadcrumbs was not executed when on the home page. I should have know that, I guess, but I’m no longer young enough to know everything. I still get to learn something new every day. Once I figured that out, I changed it to this:
if (Array.isArray(input.breadcrumbs)) {
gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
}
A simple change, but one that made it work the way that I had intended instead of the way that I had coded it. That took care of that little issue. I included the updated breadcrumbs widget in the last Update Set for my Highcharts example, but for those of you who are only interested in the breadcrumbs widget, I created a separate Update Set, which you can grab here.
Update: There is an even better version, which you can find here.