• David Tessaro

Personalizing the Portal Experience With Favorites

Updated: Oct 27, 2020

What you’ll learn in this post:

  1. How to use an angular directive to make a reusable link to add/remove an item to their favorites list.

  2. How to use an angular service to provide reusable client scripting to your widgets.

  3. How to use a scripted REST API when a widget server script isn’t available.

  4. How to include drag’n’drop functionality in your widgets.

The topic of personalizing the portal experience comes up often in my portal design workshops. The best personalized experience is one that features suggestions based on assets assigned to a user via the CMDB. However, many organizations aren’t ready to do that. Therefore, there’s not much we know about the user that allows us to make intelligent personalized suggestions.

Perhaps the next best thing is to allow users to create their own list of favorite content. Using some advanced techniques in AngularJS and Service Portal, we can provide the following features to users.



Save content as favorites from a list.

Users can add and remove favorites from a list of content, such as search results.



Save an item as a favorite from a document.

Users can mark a catalog item as a favorite when viewing the item.




Access favorites from the header menu.

Easy access to favorite content from the header menu.



Manage their favorites via a My Favorites page.

Users can change the order of their favorites by dragging these tiles around.


I demonstrated the full experience and how I built it in a recent Ask the Expert session. I’m not going to explain everything step by step in this post, since you can watch the video and see for yourself.


But as promised, I will share the code and general architecture. Here is everything I used in the video. Enjoy!

Favorites Table

The first thing you’ll need is a table to store your user’s favorites.


The fields I created on this table are:

  1. User (type = reference: sys_user)

  2. Table (type = Table Name)

  3. Document (type = Document ID, dependent on the table field)

  4. Order

Script Include

This script include is used to store server side functions that will be used by many of the other components.


var portalFavorites = Class.create();
portalFavorites.prototype = {
  initialize: function() {
  },
  
  getFavorites: function() {
    var favorites = [];
    
    var fav = new GlideRecord('u_portal_favorite');
    fav.addQuery('u_user', gs.getUserID());
    fav.orderBy('u_order');
    fav.query();
    while(fav.next()) {
      favorites.push(this.formatFavorite(fav));
    }
    
    return favorites;
  },

  isFavorite: function(table, document) {
    var isFavorite = false;

    var fav = new GlideRecord('u_portal_favorite');
    fav.addQuery('u_user', gs.getUserID());
    fav.addQuery('u_table', table);
    fav.addQuery('u_document', document);
    fav.query();
    if(fav.hasNext()) {
      isFavorite = true;
    }
    return isFavorite;
  },

  reorderFavorites: function(newIndex, oldIndex, id) {
    var i;

    var fav = new GlideRecord('u_portal_favorite');
    fav.addQuery('u_user',gs.getUserID());

    if(!oldIndex) {
      //This is a new item, so move everything down in order.
      fav.addQuery('sys_id','!=',id);
      fav.query();
      while(fav.next()) {
        i = Math.floor(fav.getValue('u_order'));
        i++;
        fav.u_order = i;
        fav.update();
      }
      return;
    }

    if(!id) {
      //An item was removed, so move everything that was behind it up in order.
      fav.addQuery('u_order','>',oldIndex);
      fav.query();
      while(fav.next()) {
        i = Math.floor(fav.getValue('u_order'));
        i--;
        fav.u_order = i;
        fav.update();
      }
      return;
    }

    if(oldIndex < newIndex) {
      //The item was moved down in order.
      //example, if item moved from 3 to 7, then get the items with indexes 4, 5, 6, & 7 and move them up in order
      fav.addQuery('sys_id','!=',id);
      fav.addQuery('u_order','>',oldIndex);
      fav.addQuery('u_order','<=',newIndex);
      fav.query();
      while(fav.next()) {
        i = Math.floor(fav.getValue('u_order'));
        i--;
        fav.u_order = i;
        fav.update();
      }
    }

    if(oldIndex > newIndex) {
      //The item was moved up in order.
      //example, if item moved from 7 to 3, then get all items with indexes 3, 4, 5, & 6 and move them down in order
      fav.addQuery('sys_id','!=',id);
      fav.addQuery('u_order','<',oldIndex);
      fav.addQuery('u_order','>=',newIndex);
      fav.query();
      while(fav.next()) {
        i = Math.floor(fav.getValue('u_order'));
        i++;
        fav.u_order = i;
        fav.update();
      }
    }

    //If a favorite was moved (not added or removed), then the last step is to update the new position of the moved item
    var item = new GlideRecord('u_portal_favorite');
    item.get(id);
    item.u_order = newIndex;
    item.update();

  },
  
  removeFavorite: function(id) {
    var fav = new GlideRecord('u_portal_favorite');
    fav.get(id);
    var oldIndex = fav.getValue('u_order');

    if(fav.deleteRecord()) {
      this.reorderFavorites(null, oldIndex);
    }
  },
  
  formatFavorite: function(fav) {
    var item = {};

    item.type = 'favorite';
    item.table = fav.getValue('u_table');
    item.document_id = fav.getValue('u_document');
    item.order = fav.getValue('u_order');
    item.sys_id = fav.getValue('sys_id');
    item.url_target = '_parent';
    
    if(item.table == 'kb_knowledge') {
      item.href = '?id=kb_article&sys_id=' + item.document_id;
      item.label = fav.u_document.short_description.getDisplayValue();
      item.image = fav.u_document.image.getDisplayValue();
      item.icon = 'fa-info';

    } else if(item.table.substring(0, 3) == 'sc_') {
      item.href = '?id=sc_cat_item&sys_id=' + item.document_id;
      
      var catItem = new GlideRecord(fav.getValue("u_table"));
      catItem.get(fav.u_document.sys_id);
      item.label = catItem.getDisplayValue("name");
      item.image = catItem.picture.getDisplayValue();
      item.icon = 'fa-shopping-cart';
        
    }

    return item;
  },

  type: 'portalFavorites'
};


Scripted REST API

I used a scripted REST API to make server side functions available to my directive and service, since Angular Providers don’t have a server script field like widgets do. Note the two resources attached to the API. That’s where this API’s scripts live.


deleteFavorite REST Resource


(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  var data = request.body.data;
  var result;

  var fav = new GlideRecord('u_portal_favorite');
  fav.addQuery('u_user', gs.getUserID());
  fav.addQuery('u_table', data.table);
  fav.addQuery('u_document', data.document);
  fav.query();
  if(fav.next()) {
    var oldIndex = fav.getValue('u_order');
    if(fav.deleteRecord()) {
      result = 'success';
      new portalFavorites().reorderFavorites(null, oldIndex);

    } else {
      result = 'fail';

    }
  } else {
    result = 'fail';

  }

  var body = {data: result};

  response.setBody(body);

})(request, response);

insertFavorite REST Resource