Service Catalog Favorites on the Service Portal

Use-Case

Whenever you use the Service Catalog and Request Fulfillment in ServiceNow, you get a highly elaborate feature set. There are ways to set up and publish multiple Service Catalogs, structure the items in various ways using Catalog Categories as well as control the visibility/accessibility of the items based on user criteria. The features of the solution seem comprehensive, although there is one trivial and quick-win feature that is missing: marking the catalog items as favorite for easier access and future reuse.

The latest release of Service Portal features a widget to show the current user’s recently viewed items and generally popular items, but this feature might not be fully suitable for all use-cases.

Features

The favorites functionality could be welcome for users who need to access certain items on a regular basis.

It is also an advantage if the solution is less space consuming, i.e., requires only a small place on a portal page and can easily be set up instead of relying on some intelligent algorithm to figure out the recent and relevant items for the current user. Therefore, it simply places the control in the hands of the end-user.

The solution I propose here is simple and intuitive and can be set up with a very basic web browsing experience.

The Solution

The solution includes two Service Portal widgets:

  • SC Category Page (ID: sc-category): If the widget is used in an out-of-the-box version, a clone needs to be created and modifications need to be made there. Then, all widget instances should be replaced using the OOB widget to use the copy. If it is already customized, only some of the code needs to be added.
  • SC Favorite Items: A new widget with any selected name, such as a customer/project specific prefix. Since this widget is brand new, nothing needs to be cloned or copied. The widget needs to be added on all portal pages where the favorites should be displayed.

SC Category Page Widget

HTML Template

The HTML template needs to be modified to render the stars as toggle and mark favorite icons. It needs to be added in the ng-repeat loop that creates the individual cards for the catalog items. Locate the div with class panel-footer (for the gray footer of the catalog item card). Add the yellow-red line as the first item inside the div.

<div aria-hidden="true" class="panel-footer">
  <div class="pull-left favicon glyphicon" ng-class="{'glyphicon-star': item.favorite, 'glyphicon-star-empty': !item.favorite}" ng-click="toggleFavorite(item)"></div>
  <a aria-hidden="true" ng-if="item.sys_class_name != 'sc_cat_item_content' || item.content_type == 'kb' || item.content_type == 'literal'" ng-click="onClick($event, item)" ng-href="{{getItemHREF(item)}}" class="pull-left text-muted" tabindex="-1">${View Details}</a>
  <a aria-hidden="true" ng-if="item.sys_class_name == 'sc_cat_item_content' && item.content_type == 'external'" ng-click="onClick($event, item)" ng-href="{{getItemHREF(item)}}" target="_blank" class="pull-left text-muted" tabindex="-1">${View Details}</a>
  <span ng-if="data.showPrices && item.hasPrice" class="pull-right item-price font-bold">{{::item.price}}</span> &nbsp;
</div>

Note that the CSS classes glyphicon is used—glyphicon-star and glyphicon-star-empty (the two are AngluarJS conditional classes). Clicking on the star icon invokes the toggleFavorite() function. Also, the item’s favorite attribute is used when working with favorites.

CSS

Because of the classes, the CSS code of the widget needs to be modified too. Add the favicon class declaration in the CSS code. Note that the CSS code doesn’t really affect the functionality; it is more similar to a cosmetic feature, so different properties can be used as compared to the example below. The most important thing here is that the icon and its placement match the look and feel of the portal, and the solution looks aesthetically pleasing.

.favicon {
    padding-right: 5px;
    margin-top: 2px;
}

The glyphicon-star/glyphicon-star-empty classes are already defined. They are valid Bootstrap glyphicons. Other glyphicons can also be used. For a list of available icons, visit this website: https://www.w3schools.com/bootstrap/bootstrap_ref_comp_glyphs.asp. Note that it’s best to use a glyphicon with two states (empty/filled, on/off, left/right, etc.).

Server Script

In the server script, the query and the loop populate the catalog items. The item’s favorite attribute needs to be initialized as a sort of binding used in the HTML template. Achieving this requires some amount of coding.

First, some variables need to be declared at the beginning of the server script, inside the function:

var arrayUtil = new ArrayUtil();
var userPrefGR = new GlideRecord("sys_user_preference");
var currentUserId = gs.getUserID();
var userPrefName = "<customer/project prefix>.sp.favcatitems";

The ArrayUtil will be used later. Then, a GlideRecord object is needed for the user preference, so that the favorites can be stored in the user preferences of each user. It requires the current user’s sys_id and finally, the name of the user preference can be declared. Note that although a naming convention has been followed in this example, the name is inconsequential and any name can therefore be used. It is important to know that this name should be noted as it is required in the next widget too.

In the next step, the user preference value needs to be queried and processed. First, the favCatItems array is declared, which contains the favorite catalog items’ sys_ids. Then, a GlideQuery is added for the current user’s preference. The user preference is a string field, a list of favorite cat item sys_ids separated by a comma. If the user already has the preference, the string needs to be converted into an array using the split() method. The code below can also go in the beginning of the server script function, right after the variable declarations.

data.favCatItems = [];

userPrefGR.initialize();
userPrefGR.addQuery("user", currentUserId);
userPrefGR.addQuery("name", userPrefName);
userPrefGR.addNotNullQuery("value");
userPrefGR.setLimit(1);
userPrefGR.query();

if (userPrefGR.next()) {
  data.favCatItems = userPrefGR.getValue("value").split(",");
}

The next step is to modify the getPopularItems() function that is used to populate the array of catalog items to be displayed. In the function, there are two loops for the GlideQueries. In both loops, where the item object’s attributes are populated, you need to add the following line (somewhere above the line that pushes the item into the items array):

item.favorite = (arrayUtil.contains(data.favCatItems, item.sys_id) ? true : false);

This checks if the current item’s sys_id is present in the current user’s favorites list/array. The attribute set here is then used in the HTML template to display an empty of a filled star.

This concludes the initialization of the favorite marks when the content is loaded. However, the server script also needs to be prepared to support toggling a favorite in order to handle the server-side of the event of clicking on the star in the catalog item’s card. For this, the following code needs to be added, right below the piece of code that populates the data.favCatItems variable:

if (input && input.action == "toggleFavorite") {
  if (input.addFav) {
    data.favCatItems.push(input.sys_id);
  } else {
    var remArray = [input.sys_id];
    data.favCatItems = arrayUtil.diff(data.favCatItems, remArray);
  }

  if (data.favCatItems.length == 0) {
    userPrefGR.deleteRecord();
  } else {
    userPrefGR.user = currentUserId;
    userPrefGR.name = userPrefName;
    userPrefGR.value = data.favCatItems.join(",");
    userPrefGR.update();
  }
}

This part is the run, when there’s a callback to the server with the name of the toggleFavorite. It checks if the item has to be added to the favorites or removed from the favorites and updates the favorites array accordingly. Also, it deletes the user preference record in the cases when the user has no more favorites left.

Client Controller

In the client controller, two functions need to be added to the bottom of the existing content inside the main function.

$scope.toggleFavorite = function(item) {
  c.server.get({
    action: "toggleFavorite",
    sys_id: item.sys_id,
    addFav: (!item.favorite ? true : false)
  }).then(function(response) {
    item.favorite = !item.favorite;
    $rootScope.$broadcast("<customer/project prefix>.update.catitemfavorites");
  });
};

The first function handles the event of clicking on the star on the article card. It calls the server’s action toggleFavorite (see the Server Script for more details), passes on the necessary parameters. In the promise, it broadcasts an event to notify the other widget (see below) about the change in the user’s favorites. Note that the name can be changed; choose whatever suits you.

$rootScope.$on("<customer/project prefix>.remove.catitemfavorites", function() { 
  c.server.update();
});

The other function is a simple one. It searches for the remove favorites event. In case it detects it, it reloads the content on the server-side. This means an update to the favorite marks on the item cards. Note that instead of $rootScope, you can also use $scope.

The two functions cover the inter-widget communication and make the two widgets update in real time.

SC Favorite Items Widget

This widget is a new one and has the following properties:

HTML Template
<div ng-show="data.favitems.length > 0" ng-class="::{'hidden-xs' : options.hide_xs}" class="panel panel-{{::options.color}} b">
  <div class="panel-heading">
    <h4 class="panel-title">
    <span ng-if="::options.glyph">
      <fa name="{{::options.glyph}}" />
    </span>{{::options.title}}</h4>
  </div>
  <div class="panel-body">
    <div class="favrow" ng-repeat="favitem in data.favitems track by $index">
      <div class="pull-left favicon glyphicon glyphicon-star" ng-click="removeFavorite(favitem)"></div>
      <a href="{{favitem.link}}">
        <span>{{ favitem.name }}</span>
      </a>
    </div>
  </div>
</div>

This part is responsible for rendering the box of favorites. Note that if the widget needs to always be displayed, even if the user has no favorites, the ng-show attribute should be removed. The title of the widget, among other things, comes from the widget instance, so the instance needs to be set up accordingly (see below).

CSS
.panel-body{
  .label{
    width: 25px;
  }
  
  .category{
    padding-bottom: 10px;
  }
  
  a{
    color: $corp-gray;
  }

  .subcategories{
    padding: 0px 0px 5px 15px;
  }
}

.favicon {
    padding-right: 5px;
    margin-top: 2px;
}

.favrow {
    padding-bottom: 5px;
}

Contains declarations for the classes used in the HTML template.

Server Script
(function) {
  
  var catItemGR = new GlideRecord("sc_cat_item");
  var contentItemGR = new GlideRecord("sc_cat_item_content");
  var userPrefGR = new GlideRecord("sys_user_preference");
  var currentUserId = gs.getUserID();
  var userPrefName = "<customer/project prefix>.sp.favcatitems";
  
  data.favitems = [];
  
  userPrefGR.initialize();
  userPrefGR.addQuery("user", currentUserId);
  userPrefGR.addQuery("name", userPrefName);
  userPrefGR.addNotNullQuery("value");
  userPrefGR.setLimit(1);
  userPrefGR.query();
  
  if (userPrefGR.next()) {
    catItemGR.initialize();
    catItemGR.addQuery("sys_id", "IN", userPrefGR.getValue("value").split(","));
    catItemGR.addActiveQuery();
    catItemGR.orderBy("name");
    catItemGR.query();
    
    while (catItemGR.next()) {
      if (contentItemGR.get(catItemGR.getValue("sys_id"))) {
        data.favitems.push({
          "sys_id": catItemGR.getValue("sys_id"),
          "name": catItemGR.getValue("name"),
          "link": contentItemGR.getValue("url")
        });
      } else {
        data.favitems.push({
          "sys_id": catItemGR.getValue("sys_id"),
          "name": catItemGR.getValue("name"),
          "link": "?id=sc_cat_item&sys_id=" + catItemGR.getValue("sys_id")
        });
      }
    }
  }
  
  if (input && input.action == "removeFavorite") {
    var newFavItemsArr = [];
    var sysPropValArr = [];
    
    for (var index = 0; index < data.favitems.length; index++) {
      if (data.favitems[index].sys_id != input.sys_id) {
        newFavItemsArr.push(data.favitems[index]);
        sysPropValArr.push(data.favitems[index].sys_id);
      }
    }
    
    data.favitems = newFavItemsArr;
    
    if (data.favitems.length == 0) {
      userPrefGR.deleteRecord();
    } else {
      userPrefGR.user = currentUserId;
      userPrefGR.name = userPrefName;
      userPrefGR.value = sysPropValArr.join(",");
      userPrefGR.update();
    }
  }
  
})();

The content of the server script of this widget is similar to what was added to the SC Category Page widget, which can be looked up to understand this one. The only difference here is in the input processing: This widget only supports removing a favorite, as the other way around would not make any sense here.

Client Controller
function($scope, $location, $rootScope) {
  /* widget controller*/
  var c = this;
  
  $rootScope.$on("<customer/project prefix>.update.catitemfavorites", function() {
    c.server.update();
  });
  
  $scope.removeFavorite = function(favitem) {
    c.server.get({"action": "removeFavorite", "sys_id": favitem.sys_id}).then(function(response) {
      var index = c.data.favitems.indexOf(favitem);
      c.data.favitems.splice(index, 1);
      
      $rootScope.$broadcast("<customer/project prefix>.remove.catitemfavorites");
    });
  };
}

The client controller is a counterpart of the new content in the SC Category Page widget’s client controller. It processes the event of updating the user’s favorite items (updates the list of favorites) and also sends out a remove favorites event notification if someone clicks on the star in the list.

Widget Instance

There are only two options in use when creating an instance of this widget—the name and the glyph. Both are self-explanatory.

Usage

The catalog browsing interface is enhanced with those small stars on each item’s card. The stars can be clicked to toggle an item as a favorite. An empty star means no favorite; a filled star means that an item has already been marked as favorite.

All kind of catalog items can be marked as favorite, so simple catalog items, record producers, order guides, content items, and the like are all supported.

As soon as there is at least one favorite, a box becomes visible, which can be freely placed across the whole portal, showing the user’s favorite items.

Clicking on a favorite item label opens the order form of the item. Clicking on the star on the left side of the favorite item label removes the favorite flag and the item will no longer be present in the favorites list.

The solution can be extended with some hints and on-screen user guides that are easy to follow; however, this is not covered in this article.

Possible Additional Features

  • The current solution displays the favorite icons if the catalog is viewed using a card view. List view is currently not supported because of the low customer demand but can be easily added using the same technique (the HTML template needs some enhancement).
  • To provide the possibility to mark other record types, e.g. categories, wizards, and so on, as favorite is also subject to customer requirements and can be achieved in a similar way.

Coming Up Next in this Topic

Catalog ordering templates: These are similar to the templates on the backend but used on the Service Portal.

About the Author

Barna Kosa is a ServiceNow Architect with a decade-long enterprise service management technology development experience. He has completed over 40 successful ServiceNow projects in multinational teams in numerous EU countries.

Updates

Blog