- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
In my learning the ServiceNow platform as a part of MetLife, one thing I have been tasked with is optimizing various client scripts on our catalog item pages to improve page-load time. A recent ServiceNow ACE report showed we had a large number of client scripts using the XMLWait function. My hope is that the scripts I am developing here will help in that optimization effort.
As I was recently working with a new catalog, I found that I would like to be able to inspect the full list of variables on the page, and since DOM inspection is a no-no in service now, I found myself confronting the need to write some script to query the various tables holding variable info.
My first approach was to try and bootstrap the data in via the g_scratchpad variable. This seemed like a great idea, but I quickly hit a brick wall due to onDisplay business rules not firing on catalog items.
So I want to share with you the scripts I developed to push my simple variable_pool data structure into the clients.
I have added a few global UI scripts to make my life easier, those being underscore.js, and loglevel.js, both are available on github, and I highly recommend them.
Script Include — CatalogVariableList.js
- Performs glide record queries against item_option_new, io_set_item to get variables, and variables sets on the catalog item
/**
Class that provide glidejax endpoint where catalog client scripts can retrieve a list of all the variables on the form
@class CatalogVariableList
@author kevin anderson
@date 8-26-2015
*/
var CatalogVariableList = Class.create();
CatalogVariableList.prototype = Object.extendsObject(AbstractAjaxProcessor, {
/**
process the catalog sysid parameter and fetch the name and sysid for each variable defined on the catalog item
@method get
@param {string} cat_item_sysid - note: parameter may also be found in the class getParameter method (ajax call)
*/
get: function(cat_item_sysid) {
var json = new JSON();
var ajax_param = json.decode(this.getParameter('sysparm_data'));
var result = {
error: 'false',
message: '',
payload: {}
};
try {
if (ajax_param && this._has(ajax_param, 'catalog_item_sysid') && this._isString(ajax_param.catalog_item_sysid)) {
// function called via ajax from client, read parameter from getParameter method
cat_item_sysid = ajax_param.catalog_item_sysid;
}
if (this._isString(cat_item_sysid) && cat_item_sysid.length) {
// get the variables defined on the catalog item
var list = this._getCatalogVariables(cat_item_sysid);
// add the variable sets assigned to this catalog item
result.payload = list.concat(this._getCatalogVariableSets(cat_item_sysid));
} else {
result.message = 'The parameter "catalog item sys_id" is not of type string';
result.error = true;
}
} catch (e) {
result.message = 'Error occured attempting to retrieve catalog item variable list - ' + e.message;
result.error = true;
}
return json.encode(result);
},
/**
get the list of variables defined on the catalog item
@method _getCatalogVariables
@param {string} sysid of the catalog item to query the variable list for
@returns {array}
*/
_getCatalogVariables: function(cat_item_sysid) {
// query for all catalog_item variables
var catalog_item_vars_rec = new GlideRecord('item_option_new');
catalog_item_vars_rec.addNotNullQuery('cat_item');
catalog_item_vars_rec.addQuery('cat_item', cat_item_sysid);
catalog_item_vars_rec.query();
var data = [],
variable_info;
while (catalog_item_vars_rec.next()) {
variable_info = {
'name': catalog_item_vars_rec.name.getDisplayValue(),
'label': catalog_item_vars_rec.getDisplayValue(),
'sys_id': catalog_item_vars_rec.sys_id.getDisplayValue(),
'set_name': '',
'set_sys_id': ''
};
//gs.log('catalog vars: '+variable_info.label+' : '+variable_info.name+' : '+variable_info.sys_id, '_getCatalogVariables::CatalogVariableList');
data.push(variable_info);
}
return data;
},
/**
get a list of all the variable sets that belong to a catalog item
@method _getCatalogSetVariableInfo
@param {string} cat_item_sysid sysid of the catalog item to query the variable list for
@returns {array} each array element is an object with the keys "sys_id" and "name"
*/
_getCatalogSetVariableInfo: function(cat_item_sysid) {
// get variable sets on the catalog
var data = [],
variable_info;
var cat_varset_rec = new GlideRecord('io_set_item');
cat_varset_rec.addNotNullQuery('cat_item');
cat_varset_rec.addQuery('sc_cat_item', cat_item_sysid);
cat_varset_rec.query();
while (cat_varset_rec.next()) {
// get the variable set name and sysid and lookup all its related variables
variable_info = {
'name': cat_varset_rec.variable_set.name.getDisplayValue(),
'sys_id': cat_varset_rec.variable_set.sys_id.getDisplayValue(),
};
if (variable_info.name && variable_info.sys_id) {
//gs.log('varset info: '+variable_info.name+' - '+variable_info.sys_id,'_getCatalogSetVariableInfo::CatalogVariableList');
data.push(variable_info);
}
}
return data;
},
/**
get list of all the variables in the variable sets that belong to a catalog item
@method _getCatalogVariableSets
@param {array} array of variable sets, each array element is an object with the keys "sys_id" and "name"
@returns {array}
*/
_getCatalogVariableSets: function(cat_item_sysid) {
this._array_polyfill();
var data = [];
var variable_sets = this._getCatalogSetVariableInfo(cat_item_sysid);
gs.log("test array : " + variable_sets.length, '_getCatalogVariableSets::CatalogVariableList');
variable_sets.forEach(function(element, index, array) {
if (this._has(element, 'sys_id') && this._isString(element.sys_id)) {
var varset_name = '';
if (this._has(element, 'name') && this._isString(element.name)) {
varset_name = element.name;
}
// query for all the variable set variables
var varset_rec = new GlideRecord('item_option_new');
varset_rec.addQuery('variable_set', element.sys_id);
varset_rec.query();
while (varset_rec.next()) {
variable_info = {
'name': varset_rec.name.getDisplayValue(),
'label': varset_rec.getDisplayValue(),
'sys_id': varset_rec.sys_id.getDisplayValue(),
'set_name': varset_name,
'set_sys_id': element.sys_id
};
//gs.log(variable_info.label+' : '+variable_info.name+' : '+variable_info.sys_id+' : '+variable_info.set_name+' : '+variable_info.set_sys_id,'_getCatalogSetVariables::CatalogVariableList');
data.push(variable_info);
}
}
}, this); // pass 'this' reference to the array.foreach method
return data;
},
/**
private method to test if an object contains a property
@method _has
@param {object} obj
@param {string} prop object key to verify exist in object
*/
_has: function(obj, prop) { // this function is not client callable
var result = false;
if (typeof obj === "object" && typeof prop === 'string' && obj && prop.length) {
if (obj.hasOwnProperty(prop)) {
result = true;
}
}
return result;
},
/**
Is a given variable an object?
@method isObject
@param {object} obj
@link https://github.com/jashkenas/underscore/blob/master/underscore.js
*/
_isObject: function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
},
/**
determine if parameter is a string
@method _isString
@param {object} str
@link https://github.com/jashkenas/underscore/blob/master/underscore.js
*/
_isString: function(str) {
return Object.prototype.toString.call(str) === '[object String]';
},
/**
polyfill for the array.each method which is an es5 component
@method _array_polyfill
@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
*/
_array_polyfill: function() {
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
var T, k;
if (this == null) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this| value as the argument.
var O = Object(this);
// 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let k be 0
k = 0;
// 7. Repeat, while k < len
while (k < len) {
var kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal method of O with argument Pk.
kValue = O[k];
// ii. Call the Call internal method of callback with T as the this value and
// argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined
};
}
}
});
UI Script — LoadCatalogItemVariablePool.js
- Provides reusable class to make Ajax call to DB and parse response
/**
query the DB via glidejax to provide the client a list of all the variables and variable sets
UI script
@class LoadCatalogItemVariablePool
@filename load_catalog_item_var_pool
Requires underscore.js and loglevel.js
*/
function LoadCatalogItemVariablePool(obj) {
// ie8 protection
if (!window.console) {
console = {
log: function() {}
}
}
// incase the loglevel module gets disabled
if (!window.log) {
window.log = {};
window.log.error = console.log;
window.log.info = console.log;
window.log.warn = console.log;
}
/**
provide browser namespace for metlife functions
@namespace metlife
*/
if (!window.metlife) {
window.metlife = {};
}
}
LoadCatalogItemVariablePool.prototype = {
constructor: LoadCatalogItemVariablePool,
/**
contents of the ajax response after json parse and extract
@property {object} data
*/
data: [],
/**
if a server-side db error message is generated, the error message from the ajax response stored here
@property {string} error_msg
*/
error_msg: '',
/**
start the process to query database for the catalog item variables and variable sets
@method getVariablePool
*/
getVariablePool: function() {
try {
// set the logging level - for production this should be "disableAll"
log.enableAll();
/**
anonymous function to maintain "this" scope for ajax callback
@method callback
@param {object} data response from ajax request
*/
var context = this;
var callback = function(data) {
context.callback_load_variable_pool(data);
};
var payload = {
'catalog_item_sysid': g_form.getParameter("sysparm_id")
};
log.info("catalog sysid: " + payload.catalog_item_sysid + ' - getVariablePool::LoadCatalogItemVariablePool');
// send the catalog item sys_id from the form to the server endpoint to feth all form variable data
var ga = new GlideAjax('CatalogVariableList');
ga.addParam('sysparm_name', 'get');
ga.addParam('sysparm_data', JSON.stringify(payload));
ga.getXML(callback);
} catch (err) {
log.error('Error occured: ' + err.message + ' - getVariablePool::LoadCatalogItemVariablePool (UI Script)');
}
},
/**
ajax callback handler to save the variable pool
value saved to the form
@method callback_load_variable_pool
@param {object} response ajax server reply
*/
callback_load_variable_pool: function(response) {
try {
var parse_result = this.parseAjaxResponse(response);
if (!parse_result) {
// parsing failed, sent the stored error message to the error handler
throw this.error_msg;
}
//log.info(this.data);
// loosely validate the response data and save to the global namespace
if (_.isArray(this.data) && this.data.length && _.isObject(this.data[0]) && _.has(this.data[0], 'sys_id')) {
window.metlife.variable_pool = this.data;
log.info('saved variable pool data to window.metlife.variable_pool - callback_load_variable_pool::LoadCatalogItemVariablePool');
// fire the event variable_pool::received after the variable pool saved to window.metlife object
// any scripts that depend on this data set will receive the notification and begin processing
// http://api.prototypejs.org/dom/Element/fire/
// http://stackoverflow.com/questions/5823782/prototype-custom-event-not-on-a-dom-element
document.fire("variable_pool::received");
} else {
throw 'unexpected format for the parsed database response object';
}
} catch (e) {
log.error('Error occured: ' + e.message + ' - callback_load_variable_pool::LoadCatalogItemVariablePool');
}
},
/**
extract the contents of the ajax response for either the error message or the payload
returns true if successfuly extracted ajax payload data
@method parseAjaxResponse
@param {string} response raw XML string from servicenow GlideAjax
@param {string} resp_key object key the repsonse data is stored under, defaults to 'payload'
@returns {boolean}
*/
parseAjaxResponse: function(response, resp_key) {
var result = false;
var ajax_data, resp_str;
if (!(_.isString(resp_key) && resp_key.length)) {
resp_key = 'payload';
}
try {
resp_str = response.responseXML.documentElement.getAttribute("answer") + '';
if (!_.isString(resp_str)) {
throw 'Ajax response is not of type "string"';
}
//log.info(resp_str + ' - parseAjaxResponse::LoadCatalogItemVariablePool');
ajax_data = JSON.parse(resp_str);
// verify the ajax parsing passed
if (!(ajax_data && _.isObject(ajax_data))) {
throw 'Ajax parsing result is not of type "object"';
}
// check for server DB error message in the ajax response
if (_.has(ajax_data, 'error') && typeof ajax_data.error === 'boolean' && ajax_data.error && _.has(ajax_data, 'message')) {
throw 'Ajax server-side error occurred: "' + ajax_data.message + '"';
}
// verify the parsed ajax response object contains the 'payload' attribute
if (!(_.has(ajax_data, resp_key) && ajax_data[resp_key] && _.isObject(ajax_data[resp_key]))) {
throw 'invalid object parameter "payload" in the ajax response';
}
// if we got here, everything is good with the ajax parsing
result = true;
this.data = ajax_data[resp_key];
} catch (e) {
this.error_msg = 'Error: ' + e.message;
}
return result;
}
};
Catalog Client Script — load_catalog_variable_pool
- Inject the UI script onto the page and instanciate the class, begin the ajax process
// include the class for performing database insert via glide ajax
document.write('<script><\/script>');
/**
perform ajax request back to db to retrieve the list of variables on this catalog
@method onload
*/
function onLoad() {
var lcvp = new LoadCatalogItemVariablePool();
lcvp.getVariablePool();
};
Catalog UI Policy — hide_device_recipient_variables_onload
- Very basic example client script that shows how the variable pool can be used
function onCondition() {
/**
when the recipient reference field is empty, hide the email and employee id fields
note:
Due to the fact the variable data is delivered via an ajax request, we may have to
create an event handler to listen for the variable data to arrive from the server
condition - user reference field is empty (default when page loads)
@method onCondition
*/
if (window._ === 'undefined') {
throw "underscore.js is undefined";
}
try {
// set the user reference related variables to hidden
if (_.has(window.metlife, 'variable_pool')) {
window.metlife.set_user_ref_fields_hidden();
} else {
// execute via callback after variable data loads from ajax call
// listen for the event notification that the variable data exists on window.metlife.variable_pool
// uses prototype custom events
document.observe("variable_pool::received", function() {
window.metlife.set_user_ref_fields_hidden()
});
}
} catch (e) {
// ie8 protection
if (!window.console) {
console = {
log: function() {}
}
};
// incase the loglevel module gets disabled
if (!window.log) {
log = {
error: console.log
};
}
log.error('Error: ' + e.message + ' - UI Policy hide recipient fields');
}
};
/**
namespace functions added for processing various form data
@property window.metlife
*/
if (!window.metlife) {
window.metlife = {};
}
/**
process the variable list and set the recipient user fields hidden
@method window.metlife.set_user_ref_fields_hidden
*/
window.metlife.set_user_ref_fields_hidden = function() {
log.info('Hide Device Recipient fields - onCondition::Catalog UI Policy');
var target_variables = ['recipient_email', 'recipient_employee_id'];
// verify the expected variables exist on the form before processing
if (_.isArray(window.metlife.variable_pool) && window.metlife.variable_pool.length) {
var observed_variables = _.pluck(window.metlife.variable_pool, 'name');
if (_.intersection(observed_variables, target_variables).length === 2) {
// set the target variable to hidden
_.each(target_variables, function(value, key) {
log.info('set variable "' + value + '" display state to hidden - window.metlife.set_user_ref_fields_hidden');
g_form.setDisplay(value, false);
});
}
}
};
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.