Skip navigation

Developer Community

4 Posts authored by: dvp

Finally I got sometime to finish off the Multiple Catalog series. In this part let's delve into search and breadcrumbs.

 

As we know the OOB Homepage and typeahead Search widget supports only single catalog. In order to support the search for multiple catalogs, we need to clone typeahead search widget and give an unique name to widget id.

 

And in server script getCatalogItems function need to updated.

 

function getCatalogItems() {

  var catalogs = [];

  var sc_cat = new GlideRecordSecure('sc_catalog');
  sc_cat.addQuery('active', true);
  sc_cat.query();

  while(sc_cat.next())
  {
  catalogs.push(sc_cat.sys_id.toString());
  }


  var sc = new GlideRecord('sc_cat_item');
  sc.addQuery(textQuery, data.q);
  sc.addQuery('active',true);
  sc.addQuery('no_search', '!=', true);
  sc.addQuery('visible_standalone', true);
  sc.addQuery('sys_class_name', 'NOT IN', 'sc_cat_item_wizard');
  sc.addQuery('sc_catalogs', catalogs);
  sc.query();

  var catCount = 0;
  while (sc.next() && catCount < data.limit) {
  if (!$sp.canReadRecord(sc))
  continue;

  var item = {};
  if (sc.getRecordClassName() == "sc_cat_item_guide")
  item.type = "sc_guide";
  else if (sc.getRecordClassName() == "sc_cat_item_content") {
  var gr = new GlideRecord('sc_cat_item_content');
  gr.get(sc.getUniqueValue());
  $sp.getRecordValues(item, gr, 'url,content_type,kb_article');
  item.type = "sc_content";
  }
  else
  item.type = "sc";

  $sp.getRecordDisplayValues(item, sc, 'name,short_description,picture,price,sys_id');
  item.score = parseInt(sc.ir_query_score.getDisplayValue());
  item.label = item.name;
  data.results.push(item);
  catCount++;
  }
}

Once this widget is created, use the new widget ID in homepage search widget

 

data.typeAheadSearch = $sp.getWidget('new-typeahead-search', typeAheadSearchOpts);

 

Once the widget is successfully created use the new typeahead search widget across all the pages.

 

Breadcrumbs

 

To point the correct breadcrumbs we have update the client controller in following widgets

  • Multi SC categories widget that was created earlier
  • SC Category Page widget
  • SC catalog Item widget

 

Here is the sample script for multi SC categories client controller

 

var bc = [{label: 'Catalogs', url: '?id=multi_catalog'}];

 

if ($scope.data.sc_catalog) {

bc[bc.length] = {label: $scope.data.catalog_name, url: '#'};

}    

$rootScope.$broadcast('sp.update.breadcrumbs', bc);

 

 

 

This concludes multiple catalog functionality in Service Portal.

 

 

Previous Blogs in this series

Portal diaries: Service Portal – Multiple Catalogs (Part 1)

Portal diaries: Service Portal – Multiple Catalogs (Part 2)

Today, I would like to explain how to only display the categories that are related to a catalog.

 

For this we need to update SC categories widget, SC Category Page as well as SC Popular items OOB widgets.

 

Before going into the widget, I would like to shed some light on a method available in $sp API getParameter returns the value of given key. This methods helps to retrieve the sys id of the selected catalog.

 

If you have observed in the part 1 of this article series, the anchor tag has the sys id of catalog in URL.

<a ng-href="?id=sc_home2&catalog_sys_id={{inc.sysid}}" class="panel-body block">

 

And $sp.getParameter('catalog_sys_id');  will get the sys id of the catalog.

 

Once we have the sys id of the catalog all we need to do is update the existing widgets with the above method and filter the categories.

 

Below is the Updated server code of the SC categories widget

 

(function() {
// populate the 'data' object
data.categories = [];

data.sc_catalog = $sp.getParameter('catalog_sys_id'); //$sp.getValue('sc_catalog');
   
  var sc_cat = new GlideRecord('sc_catalog');
  sc_cat.addQuery('sys_id', data.sc_catalog);
  sc_cat.query();


  while(sc_cat.next())
  {
  data.catalog_name = sc_cat.title.toString();
  } 

options.category_layout = options.category_layout || "Nested";


if (options.page) {
  var pageGR = new GlideRecord("sp_page");
  options.page = (pageGR.get(options.page)) ? pageGR.getValue("id") : null;
} else {
  options.page = 'sc_category';
}


if (input && input.action === "retrieve_nested_categories") {
  var childCategoriesGR = buildSubcategoryGR(input.parentID);
  data.subcategories = retrieveCategoriesFromGR(childCategoriesGR);
  return ;
}


var sc = new GlideRecord('sc_category');
sc.addQuery('sys_class_name', 'sc_category');
sc.addActiveQuery();
sc.orderBy('title');
//data.sc_catalog = $sp.getParameter('catalog_sys_id'); //$sp.getValue('sc_catalog');
if (data.sc_catalog)
  sc.addQuery('sc_catalog', data.sc_catalog);
if (options.category_layout === "Nested")
  sc.addQuery('parent', '');
sc.query();
data.categories = retrieveCategoriesFromGR(sc);


// If the selected category is a subcategory, we need to 
// open all it's parent categories
var selectedCategory = new GlideRecord("sc_category");
var categoryID = $sp.getParameter("sys_id");
if (!categoryID || !selectedCategory.get(categoryID))
  return;


var parentArr;
if (options.category_layout !== "Nested" || !selectedCategory.parent)
  parentArr = data.categories;
else
  parentArr = openParent(selectedCategory.getElement("parent").getRefRecord());


var selectedCategoryItem = findElementBySysID(parentArr, selectedCategory.getUniqueValue());
if (selectedCategoryItem)
  selectedCategoryItem.selected = true;


function openParent(gr) {
  var catItem;

  if (!gr.parent) {
  catItem = findElementBySysID(data.categories, gr.getUniqueValue());
  } else {
  var parentCategoryArr = openParent(gr.getElement("parent").getRefRecord());
  catItem = findElementBySysID(parentCategoryArr, gr.getUniqueValue());
  }

  if (!catItem)
  return [];

  var subcategoryGR = buildSubcategoryGR(catItem.sys_id);
  catItem.subcategories = retrieveCategoriesFromGR(subcategoryGR);
  catItem.showSubcategories = true;
  return catItem.subcategories;
}


function findElementBySysID(arr, id) {
  var foundElements = arr.filter(function(item) {
  return item.sys_id === id;
  });

  return (foundElements.length > 0) ? foundElements[0] : null;
}


function retrieveCategoriesFromGR(gr) {
  var categories = []
  while (gr.next()) {
  var category = retrieveCategoryFromGR(gr);
  if (category)
  categories.push(category);
  }

  return categories;
}


function retrieveCategoryFromGR(gr) {
  if (!$sp.canReadRecord("sc_category", gr.getUniqueValue()))
  return null;


  if (options.check_can_view != true && options.check_can_view != "true") {
  // use GlideAggregate by way of GlideRecordCounter, doesn't check canView on each item
  var count = new GlideRecordCounter('sc_cat_item_category');
  prepQuery(count, gr.getUniqueValue());
  var item_count = count.getCount();
  if (item_count > 0) {
  var cat = {};
  cat.title = gr.title.getDisplayValue();
  cat.sys_id = gr.getUniqueValue();
  cat.catalog_sys_id = gr.sc_catalog.sys_id.toString();
  cat.count = item_count;
  cat.parent = gr.parent.getDisplayValue();
  if (options.category_layout === "Nested")
  cat.isParentCategory = checkIsParentCategory(gr);
  return cat;
  }
  }


  if (options.check_can_view == true || options.check_can_view == "true") {
  // use GlideRecord, checking canView on each item
  var itemCat = new GlideRecord('sc_cat_item_category');
  prepQuery(itemCat, gr.getUniqueValue());
  itemCat.query();
  var validatedCount = 0;
  var checked = 0;
  while (itemCat.next()) {
  checked++;
  if ($sp.canReadRecord("sc_cat_item", itemCat.sc_cat_item))
  validatedCount++;


  // if user can't see the first 50 items in this category, give up
  if (validatedCount == 0 && checked == 50)
  break;


  // if omitting badges, and if we found one, work is done
  if (validatedCount > 0 && options.omit_badges)
  break;
  }


  if (validatedCount > 0) {
  var cat = {};
  cat.title = gr.title.getDisplayValue();
  cat.sys_id = gr.getUniqueValue();
  cat.catalog_sys_id = gr.sc_catalog.sys_id.toString();
  cat.count = validatedCount;
  cat.parent = gr.parent.getDisplayValue();
  if (options.category_layout === "Nested")
  cat.isParentCategory = checkIsParentCategory(gr);
  return cat;
  }
  }

  return null;
}


function prepQuery(gr, scUniqueValue) {
  gr.addQuery('sc_category', scUniqueValue);
  gr.addQuery('sc_cat_item.active', true);
  gr.addQuery('sc_cat_item.visible_standalone', true);
  gr.addQuery('sc_cat_item.sys_class_name', 'NOT IN', 'sc_cat_item_wizard');
}


function checkIsParentCategory(cat) {
  var count = new GlideRecordCounter('sc_category');
  count.addQuery('active', true);
  count.addQuery('parent', cat.getUniqueValue());
  return count.getCount() > 0;
}


function buildSubcategoryGR(parentID) {
  var subcategoryGR = new GlideRecord("sc_category");
  subcategoryGR.addActiveQuery();
  subcategoryGR.orderBy('title');
  var sc_catalog = data.sc_catalog; //$sp.getValue('sc_catalog');
  if (sc_catalog)
  subcategoryGR.addQuery('sc_catalog', sc_catalog);
  subcategoryGR.addQuery('parent', parentID);
  subcategoryGR.query();
  return subcategoryGR;
}
})();

 

 

Also, we need to update spCategoryListItem angular provider to show only the categories of a selected catalog

 

Below is the updated client script of category angular provider

function spCategoryListItem() {
return {
restrict: 'E',
scope: {
category: "=",
omitBadges: "=",
level: "=",
page: "=?"
},
replace: true,
template:   '<div ng-class="{\'indent-category\': indentCategory}">' +
'<a class="list-group-item" ng-class="{selected: category.selected}" href="?id={{page}}&sys_id={{::category.sys_id}}&catalog_sys_id={{::category.catalog_sys_id}}">' +
'<span ng-if="!omitBadges" class="label label-as-badge label-primary">{{::category.count}}</span>' +
'<i class="fa fa-fw text-muted" ng-class="{\'fa-folder\': !category.showSubcategories, \'fa-folder-open\': category.showSubcategories}" ng-if="category.isParentCategory" ng-click="toggleShowSubcategories($event)"></i>{{::category.title}}' +
'</a>' +
'<sp-category-list-item ng-if="category.showSubcategories" ng-repeat="subcategory in category.subcategories" category="subcategory" omit-badges="omitBadges" level="level + 1"></sp-category-list-item>' +
'</div>',
controller: function($scope) {
// We have to eventually stop indenting the categories.
// So, we're choosing to indent up to 3 times. Otherwise,
// there won't be enough room to show the category name.
$scope.indentCategory = ($scope.level > 0 && $scope.level < 4);
$scope.page = $scope.page || 'sc_category';
$scope.toggleShowSubcategories = function(e) {
e.originalEvent.stopPropagation();
e.originalEvent.preventDefault();

$scope.$emit("$sp.sc_category.retrieve_subcategories", $scope.category);
$scope.category.showSubcategories = !$scope.category.showSubcategories;
}
}
}
}

 

Once we have the widget ready, create a new page with id sc_home2 and add updated SC category widget the the left…

One may follow the similar steps to get the popular items. But keep in mind that existing popular items widget will only work for catalog items and not for record producers.

 

Screenshot pointing the the page id of a catalog homepage

 

                                                                                                  Catalog Homepage

 

 

Note: If a different name is used for page id other than sc_home2, update the same in multi catalog widget that was created in part 1 of this article

 

 

After user landed on the desired Catalog homepage and when the user picks any category on the left then URL will redirect to sc_category page. Below is the screenshot of the sc_category page with the widgets.

In order to support multi portal functionality, the category page needs to be updated with the widget mentioned below.

 

 

Below is the updated code for SC Category Page Widget

 

Server Code

(function() {
  data.category_id = $sp.getParameter("sys_id");
  data.showPrices = $sp.showCatalogPrices();
  if (options && options.sys_id)
  data.category_id = options.sys_id;


  data.sc_catalog = $sp.getParameter('catalog_sys_id');

  var sc_cat = new GlideRecord('sc_catalog');
  sc_cat.addQuery('sys_id', data.sc_catalog);
  sc_cat.query();


  while(sc_cat.next())
  {
  data.catalog_name = sc_cat.title.toString();
  data.catalog_url = 'sc_home2&catalog_sys_id=' + sc_cat.sys_id.toString();


  } 

// data.sc_catalog_page = $sp.getDisplayValue("sc_catalog_page") || "oa_sc_home2";
  // Does user have permission to see this category?
  if (!$sp.canReadRecord("sc_category", data.category_id)) {
  data.error = "You do not have permission to see this category";
  return;
  } 


  var cat = new GlideRecord('sc_category');
  cat.get(data.category_id);
  data.category = cat.getDisplayValue('title');
  var items = data.items = [];
  var sc = new GlideRecord('sc_cat_item_category');
  if (data.category_id) 
  sc.addQuery('sc_category', data.category_id);


  sc.addQuery('sc_cat_item.active',true);
  sc.addQuery('sc_cat_item.sys_class_name', 'NOT IN', 'sc_cat_item_wizard');
  sc.orderBy('sc_cat_item.order');
  sc.orderBy('sc_cat_item.name');
  sc.query();
  while (sc.next()) {
  // Does user have permission to see this item?
  if (!$sp.canReadRecord("sc_cat_item", sc.sc_cat_item.sys_id.getDisplayValue()))
  continue;


  var item = {};
  var gr = new GlideRecord('sc_cat_item');
  gr.get(sc.sc_cat_item);
  gr = GlideScriptRecordUtil.get(gr).getRealRecord();
  $sp.getRecordDisplayValues(item, gr, 'name,short_description,picture,price,sys_id');
  item.sys_class_name = sc.sc_cat_item.sys_class_name + "";
  item.page = 'sc_cat_item';
  if (item.sys_class_name == 'sc_cat_item_guide')
  item.page = 'sc_cat_item_guide';
  else if (item.sys_class_name == 'sc_cat_item_content') {
  $sp.getRecordValues(item, gr, 'url,content_type,kb_article');
  if (item.content_type == 'kb') {
  item.page = 'kb_article';
  item.sys_id = item.kb_article;
  } else if (item.content_type == 'literal') {
  item.page = 'sc_cat_item';
  } else if (item.content_type == 'external')
  item.target = '_blank';
  }


  items.push(item);
  }
})()

 

 

Once we have all the widgets needed, all we need to do is use the updated widgets on the page.

 

For this, follow the below steps

  1. Open sc_category page
  2. Open the Instance of SC categories widget and update the widget  reference field with multi categories widget that was created earlier.

 

 

this concludes the part 2 of multi catalog functionality in Service Portal. In the next part, I will delve into Search and Breadcrumbs functionalities that supports multi catalogs.

 

 

Attached are the XML files of the following Widgets

  • Multi SC Categories
  • Multi SC Popular Items
  • Multi SC Category Page

 

Blogs in this series:

Portal diaries: Service Portal – Multiple Catalogs (Part 1)

Here is the second article in the series of Portal diaries. In this article, I would like to discuss about displaying the summary of approval record in Service Portal. As customers started using ServiceNow widely the idea of approval process drifted into other modules like Resource plans, Issues, stories, hr case, test plans which are just a few to mention.

 

In my opinion the OOB approval page in Service Portal is only focusing on RITM approval displaying all the variables whereas for other tables it is only showing the following four fields (Short description, Opened by, Start and End dates if applicable).

 

Backend form view of ServiceNow approval record displays the approving field summary with the help of approval summarizer UI macro. If the specific table has approval view the UI macro (Approval Summarizer) displays fields of this view, if not the default view of form is shown.

 

Here is the screenshot of a change request fields in an approval record using approval summarizer.

 

This similar solution can be implemented on Service Portal via a custom script include taking GlideReocrd as an input and outputting the fields of a default view along with its field positions. When I was working on a different requirement I came across one of the methods in $sp portal API called getForm and a <sp-model> tag to the render form.

 

Here is the format for calling getForm api and using it in HTML view

 

HTML Template

<sp-model form-model="data.f" mandatory="mandatory"></sp-model>

 

Server Code

data.f = $sp.getForm( table_name, sys_id_of_record);

 

This solution seemed clean until I realized that the getForm method display fields with edit access  which is undesired. But on a positive note getForm method returns the fields after all the rules like UI policies, client scripts, ACL’s and even dictionary level read only. This generates me an idea of forcing all the fields to be read only .

 

In order to make all the fields read only, I hacked the data.f object and updated the sys_readonly property.

 

Here is the server script to make all the fields of a record read only

 

data.f = $sp.getForm( table_name, sys_id);

for (var field_name in data.f._fields) {
     data.f._fields[field_name]["sys_readonly"] = true;
}


 

Screenshot of Story form in approval widget

 

Screenshot of HR case form in approval widget

 

Here is more info about what all the getForm returns

 

gs.log('display value: ' + data.f.display_value)

gs.log('ui acations: ' + data.f._ui_actions)

gs.log('short description: ' + data.f.short_description)

gs.log('plural : ' + data.f.plural)

gs.log('view_titile: ' + data.f.view_title)

gs.log('_pref: ' + data.f._perf)

gs.log('_sections: ' + data.f._sections)

gs.log('label: ' + data.f.label)

gs.log('title: ' + data.f.title)

gs.log('_fields: ' + data.f._fields)

gs.log('_formatters: ' + data.f._formatters)

gs.log('sys_id: ' + data.f.sys_id)

gs.log('view: ' + data.f.view)

gs.log('scratchpad: ' + data.f.g_scratchpad)

gs.log('_view: ' + data.f._view)

gs.log('attachmentGUID: ' + data.f._attachmentGUID)

gs.log('client_script: ' + data.f.client_script)

gs.log('related_list: ' + data.f._related_lists)

gs.log('table: ' + data.f.table)

gs.log('policy: ' + data.f.policy)

 

As in this article I talked more about properties of fields. Here are the all the properties of a field

 

gs.log('sys_mandatory : ' + data.f._fields.cmdb_ci.sys_mandatory)

gs.log('visible : ' + data.f._fields.cmdb_ci.visible)

gs.log('dependentField : ' + data.f._fields.cmdb_ci.dependentField)

gs.log('dbType : ' + data.f._fields.cmdb_ci.dbType)

gs.log('label : ' + data.f._fields.cmdb_ci.label)

gs.log('sys_readonly : ' + data.f._fields.cmdb_ci.sys_readonly)

gs.log('type : ' + data.f._fields.cmdb_ci.type)

gs.log('mandatory : ' + data.f._fields.cmdb_ci.mandatory)

gs.log('refTable : ' + data.f._fields.cmdb_ci.refTable)

gs.log('displayValue : ' + data.f._fields.cmdb_ci.displayValue)

gs.log('readonly : ' + data.f._fields.cmdb_ci.readonly)

gs.log('hint : ' + data.f._fields.cmdb_ci.hint)

gs.log('name : ' + data.f._fields.cmdb_ci.name)

gs.log('attributes : ' + data.f._fields.cmdb_ci.attributes)

gs.log('reference_key : ' + data.f._fields.cmdb_ci.reference_key)

gs.log('readonlyClickthrough : ' + data.f._fields.cmdb_ci.readonlyClickthrough)

gs.log('choice : ' + data.f._fields.cmdb_ci.choice)

gs.log('value : ' + data.f._fields.cmdb_ci.value)

gs.log('max_length : ' + data.f._fields.cmdb_ci.max_length)

gs.log('ed : ' + data.f._fields.cmdb_ci.ed)

 

Please find the attached XML of updated Approval Record widget.

 

Edit: updated the widget to include record attachments

 

Previous blogs in this series

Portal diaries: Service Portal – Making Rejection Comments Mandatory on Approval Record

Portal diaries: Service Portal – Multiple Catalogs (Part 1)

In this series, I would like to share the solutions that are missing in Service Portal. Today I would to discuss about how to make rejection comments mandatory in Service Portal.

 

Approvals in ServiceNow are handled either via emails or through an approval record. One of the significant reasons to use approvals via form is the ability to capture comments when rejecting a record. In a form view there is an OOB UI policy Comments mandatory on rejection, that makes comments field mandatory while rejecting a record.

 

When I initially started playing with Portal, I did not find a way to capture comments in OOB approval widgets. So I cloned the OOB Approval info widget and added a new text box

<textarea ng-model="c.data.comment" style="color: grey; width: 100%; margin-top: .5em;" placeholder="Rejection Comments" class="form-control" rows="5"></textarea>

 

Below  is the screenshot of comments box

 

In order to make the comments field required while rejecting, first validate whether it is empty or not. If it is then stop the approval process.

After adding couple lines in server and client controller I'm able stop the rejection when the comments are NULL and an alert box will pop out as shown in the image below.

 

Here is the code that has to be updated in server and client controller blocks of a cloned widget.

 

Server Code

 

if (input.comment){
     gr.comments = input.comment;
     gr.update();
}

 

Client Controller

 

c.action = function(state) {

     if( (c.data.comment ==  undefined || c.data.comment ==  '' )&& state == 'rejected'){
          $window.alert('Rejection Comments cannot be empty');
          return false;

     }
     c.data.op = state;
     c.data.state = state;
     c.server.update();
}

 

 

 

Please find the attached XML of updated Approval info widget.

 

 

Blogs in this series

Portal diaries: Service Portal – Approval Summarizer in Portal

Portal diaries: Service Portal – Multiple Catalogs (Part 1)

Filter Blog

By date: By tag: