@mentions in the Service Portal

“Don’t worry if it doesn’t work right. If everything did, you’d be out of a job.”
Mosher’s Law of Software Engineering

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:

Adding an @mention to a form field

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:

<textarea
  id="activity-stream-comments-textarea"
  aria-label="{{activity_field_1.label}}"
  class="sn-string-textarea form-control"
  placeholder="Additional comments"
  data-stream-text-input="comments"
  ng-required="activity_field_1.mandatory && !activity_field_1.filled"
  ng-model="activity_field_1.value"
  ng-attr-placeholder="{{activity_field_1.label}}"
  sn-sync-with="activity_field_1.name"
  sn-sync-with-value-in-fn="reduceMentions(text)"
  sn-sync-with-value-out-fn="expandMentions(text)"
  mentio=""
  mentio-id="'activity-stream-comments-textarea'"
  mentio-typed-term="typedTerm"
  mentio-require-leading-space="true"
  mentio-trigger-char="'@'"
  mentio-items="members"
  mentio-search="searchMembersAsync(term)"
  mentio-template-url="/at-mentions.tpl"
  mentio-select="selectAtMention(item)"
  mentio-suppress-trailing-space="true"
  sn-resize-height="">
</textarea>

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:

Basic textarea for @mentions enablement

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:

  mentio-search="searchMembersAsync(term)"
  mentio-template-url="/at-mentions.tpl"
  mentio-select="selectAtMention(item)"

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:

$scope.searchMembersAsync = function(term) {
$scope.members = [];
$scope.members.loading = true;
$timeout.cancel(typingTimer);
if (term.length === 0) {
$scope.members = [{
termLengthIsZero: true
}];
$scope.members.loading = false;
} else {
typingTimer = $timeout(function() {
snMention.retrieveMembers($scope.table, $scope.sysId, term).then(function(members) {
$scope.members = members;
$scope.members.loading = false;
}, function () {
$scope.members = [{
termLengthIsZero: true
}];
$scope.members.loading = false;
});
}, 500);
}
};

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:

function($scope, $timeout, snMention, spModal) {
	var c = this;
	var typingTimer;

	$scope.snMention = snMention;
	$scope.macros = {};
	$scope.mentionMap = {};
	$scope.members = [];
	$scope.theTextArea = '';

	$scope.searchMembersAsync = function(term) {
		$scope.members = [];
		$scope.members.loading = true;
		$timeout.cancel(typingTimer);
		if (term.length === 0) {
			$scope.members = [{
				termLengthIsZero: true
			}];
			$scope.members.loading = false;
		} else {
			typingTimer = $timeout(function() {
				snMention.retrieveMembers('sys_id', c.data.userId, term).then(function(members) {
					$scope.members = members;
					$scope.members.loading = false;
				}, function () {
					$scope.members = [{
						termLengthIsZero: true
					}];
					$scope.members.loading = false;
				});
			}, 500);
		}
	};

	$scope.selectAtMention = function(item) {
		if (item.termLengthIsZero) {
			return (item.name || "") + "\n";
		}
		$scope.mentionMap[item.name] = item.sys_id;
		return "@[" + item.name + "]";
	};
}

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:

<link rel="stylesheet" type="text/css" href="/styles/heisenberg/heisenberg_all.cssx">
<link rel="stylesheet" type="text/css" href="/css_includes_ng.cssx">

Now we’re cooking with gas!

What do you know … it actually works!

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:

List of Users mentioned in the text

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.