
- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on ‎09-16-2020 08:37 AM
It's upgrade time, and I'm reviewing "skips" and loving it!
One of the things that me and my team find challenging is reviewing updates to forms and lists because when you do the side-by-side comparison, you're looking at the XML for the update. It's a lot of data that masks the information that I'm looking for.
I've created a UI action for the Upgrade details record that makes it easier to to the comparison. Instead of looking at the content above you get to see the layout elements side-by-side in a table. When the UI Action is in place, you'll get this when you run it on a Upgrade details record for a form layout, list layout, or related lists layout.
The UI Action
Create a UI Action with the following properties:
- Name: Show List and Layout Diffs Simplified
- Table: Upgrade Detaisl [sys_upgrade_history_log]
- Select these check boxes
- Active
- Show insert
- Show update
- Form context menu
- Script: <see below>
UI Action Script
/*_________________________________________________________________
* Description: Creates an HTML table that shows the differences between what's on two layouts.
* Parameters: None
* Returns: Nothing
* Sets: Nothing
________________________________________________________________*/
function myCompare() {
if (typeof scriptName == "undefined") {
// we will add the scriptName to the function Name
scriptName = "";
}
var functionName = "Function: myCompare\n";
functionName = scriptName + functionName;
try {
// Put content here
// Create an object that will have some functions we need
var o = {};
o.getRelationship = function(rel) {
var id = rel.replace(/REL:/, "");
var gr = new GlideRecord("sys_relationship");
if (gr.get(id)) {
return gr.getValue("name");
} else {
return rel;
}
};
o.getRecXml = function(name, state, source) {
if (gs.nil(name) || gs.nil(state)) {
return null;
}
var gr = new GlideRecord("sys_update_version");
gr.addQuery("name", name);
gr.addQuery("state", state);
if (!gs.nil(source)) {
gr.addQuery("source", historyId);
}
gr.orderByDesc("sys_updated_on");
gr.query();
gr.next();
var xml = GlideappUpdateVersion.replacePayloadWithPreviewXML(gr);
return xml;
};
o.getObj = function(record) {
//gs.addInfoMessage(record.payload);
var recordObj = new XMLHelper(record.payload).toObject();
if (recordObj.hasOwnProperty("sys_ui_related_list_entry")) {
return this.getRelatedListObj(recordObj);
} else if (recordObj.hasOwnProperty("sys_ui_element")) {
return this.getFormObj(recordObj);
} else if (recordObj.hasOwnProperty("sys_ui_list_element")) {
return this.getListObj(recordObj);
} else {
this.showMessage(recordObj);
}
};
o.getListObj = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_ui_list_element;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = entry.position;
var entryName = entry.element;
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getFormObj = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_ui_element;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = entry.position;
var entryName = entry.element;
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getRelatedListObj = function(recordObj) {
var entriesObj = {};
var listEntries = recordObj.sys_ui_related_list_entry;
for (var i = 0; i < listEntries.length; i++) {
var entry = listEntries[i];
var position = entry.position;
var entryName = this.getRelationship(entry.related_list);
entriesObj[position] = entryName;
}
return entriesObj;
};
o.createRow = function(cObj, uObj, index) {
var rowTemplate = "<tbody><tr><td>{0}</td><td>{1}</td><td>{2}</td></tr></tbody>";
var params = [];
params.push(" " + index + " ");
var leftCell = (uObj.hasOwnProperty(index + "")) ? uObj[index + ""] : "----";
var rightCell = (cObj.hasOwnProperty(index + "")) ? cObj[index + ""] : "----";
params.push(" " + leftCell + " ");
params.push(" " + rightCell + " ");
var row = gs.getMessage(rowTemplate, params);
return row;
};
o.createTable = function(header, rows) {
var tableTemplate = "<table>{0}{1}</table>";
var params = [];
params.push(header);
params.push(rows);
var table = gs.getMessage(tableTemplate, params);
return table;
};
o.createHeader = function(cName, uName, index) {
var rowTemplate = "<thead><tr><th>{0}</th><th>{1}</th><th>{2}</th></tr></thead>";
var params = [];
params.push(" " + index + " ");
params.push(" " + uName + " ");
params.push(" " + cName + " ");
var row = gs.getMessage(rowTemplate, params);
return row;
};
o.messageTemplate = messageTemplate = "<pre>{0}</pre>";
o.showMessage = function(obj) {
var objString = JSON.stringify(obj, undefined, 2);
gs.addInfoMessage(gs.getMessage(this.messageTemplate, objString));
};
/*******************
// Here's where we do the work
*******************/
var upgradeId = current.getUniqueValue();
var hlSysId = upgradeId;
var gr = new GlideRecord("sys_upgrade_history_log");
gr.addQuery("sys_id", hlSysId);
gr.query();
if (!gr.next()) {
return;
}
var historyId = gr.getValue("upgrade_history");
var currentXml = o.getRecXml(gr.getValue("file_name"), "current");
var upgradeXMl = o.getRecXml(gr.getValue("file_name"), "history", historyId);
var currentObj = o.getObj(currentXml);
var upgradeObj = o.getObj(upgradeXMl);
var length = Object.keys(currentObj).length;
length = (length > Object.keys(upgradeObj).length) ? length : Object.keys(upgradeObj).length;
var cName = "Our Version";
var uName = "Upgrade Version";
var tHead = "";
tHead = o.createHeader(cName, uName, "position");
var tRows = "";
for (var i = 0; i < length; i++) {
tRows += (o.createRow(currentObj, upgradeObj, i));
}
var table = o.createTable(tHead, tRows);
var winContent = '<textarea>[code]{0}[/code]</textarea>';
gs.addInfoMessage(gs.getMessage(winContent, table));
gs.addInfoMessage(gs.getMessage(table));
} catch (e) {
// enter what you want to happen if there is an error here
var errorMessage = functionName + e;
gs.addInfoMessage(errorMessage);
gs.log(errorMessage);
}
}
myCompare();
action.setRedirectURL(current);
Output of the UI Action
When you click on the UI action, it uses gs.addInfoMessage() to display a table like the one shown earlier and a small <textarea> containing the HTML for the table enclosed in the [code][/code] tags. This table can be added to a comment or worknote on a task.
Let me know if you have any questions.
Thanks,
Cody
- 2,158 Views

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks Cody. I had an external script which had a part of this functionality. I wanted to extend it and incorporate it in ServiceNow, but thanks to you this will save me time. And during the upgrade it will help us as well 🙂
Thanks!
Kind regards,
Gerard Wijnands

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I've added a function next to the mycompare function:
function searchObj (obj, query) {
for (var key in obj) {
var value = obj[key];
if (value === query) {
return true;
}
}
return false;
}
and I changed the createRow function to:
o.createRow = function(cObj, uObj, index) {
var rowTemplate = "<tbody><tr><td>{0}</td><td>{1}</td><td>{2}</td></tr></tbody>";
var params = [];
params.push(" " + index + " ");
var leftCell = (uObj.hasOwnProperty(index + "")) ? uObj[index + ""] : "----";
var rightCell = (cObj.hasOwnProperty(index + "")) ? cObj[index + ""] : "----";
if (!searchObj(cObj,leftCell)) {
leftCell="<span style='background-color: Coral'>"+leftCell+"</span>";
}
if (!searchObj(uObj,rightCell)) {
rightCell="<span style='background-color: Chartreuse'>"+rightCell+"</span>";
}
params.push(" " + leftCell + " ");
params.push(" " + rightCell + " ");
var row = gs.getMessage(rowTemplate, params);
return row;
};
In this way the differences are more visible:

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Gerard,
Great addition!
Thanks,
Cody
Updated version of the script with Gerard's changes. And with functions for handling sys_ui_form_sections and sys_choice updates.
/*_________________________________________________________________
* Description:
* Parameters:
* Returns:
* Sets:
________________________________________________________________*/
function myCompare() { // replace myCompare with the name fo your function
if (typeof scriptName == "undefined") {
// we will add the scriptName to the function Name
scriptName = "";
}
var functionName = "Function: myCompare\n";
functionName = scriptName + functionName;
try {
// Put content here
var upgradeId = current.getUniqueValue();
var hlSysId = upgradeId; //this.getParameter("sysparm_history_log");
var gr = new GlideRecord("sys_upgrade_history_log");
gr.addQuery("sys_id", hlSysId);
gr.query();
if (!gr.next()) {
return;
}
// Secured.
if (!gr.canRead()) {
gs.logWarning("Security restricted: cannot read", "DiffAjax.diffHistoryLogToCurrentVersion");
return;
}
var historyId = gr.getValue("upgrade_history");
var o = {};
o.getRelationship = function(rel) {
var id = rel.replace(/REL:/, "");
var gr = new GlideRecord("sys_relationship");
if (gr.get(id)) {
return gr.getValue("name");
} else {
return rel;
}
};
o.getRecXml = function(name, state, source) {
if (gs.nil(name) || gs.nil(state)) {
return null;
}
var gr = new GlideRecord("sys_update_version");
gr.addQuery("name", name);
gr.addQuery("state", state);
if (!gs.nil(source)) {
gr.addQuery("source", historyId);
}
gr.orderByDesc("sys_updated_on");
gr.query();
gr.next();
var xml = GlideappUpdateVersion.replacePayloadWithPreviewXML(gr);
return xml;
};
o.getObj = function(record) {
//gs.addInfoMessage(record.payload);
var recordObj = new XMLHelper(record.payload).toObject();
if (recordObj.hasOwnProperty("sys_ui_related_list_entry")) {
return this.getRelatedListObj(recordObj);
} else if (recordObj.hasOwnProperty("sys_ui_element")) {
return this.getFormObj(recordObj);
} else if (recordObj.hasOwnProperty("sys_ui_list_element")) {
return this.getListObj(recordObj);
} else if (recordObj.hasOwnProperty("sys_ui_form_section")) {
return this.getFormSection(recordObj);
} else if (recordObj.hasOwnProperty("sys_choice")) {
return this.getChoices(recordObj);
} else {
this.showMessage(recordObj);
}
};
o.getChoices = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_choice;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = i;
var entryName = entry.label + "-" + entry.value + "";
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getFormSection = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_ui_form_section;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = entry.position;
var entryName = entry.sys_ui_section["@display_value"];
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getListObj = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_ui_list_element;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = entry.position;
var entryName = entry.element;
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getFormObj = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_ui_element;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = entry.position;
var entryName = entry.element;
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getRelatedListObj = function(recordObj) {
var entriesObj = {};
var listEntries = recordObj.sys_ui_related_list_entry;
for (var i = 0; i < listEntries.length; i++) {
var entry = listEntries[i];
var position = entry.position;
var entryName = this.getRelationship(entry.related_list);
entriesObj[position] = entryName;
}
return entriesObj;
};
o.createRow = function(cObj, uObj, index) {
var rowTemplate = "<tbody><tr><td>{0}</td><td>{1}</td><td>{2}</td></tr></tbody>";
var params = [];
params.push(" " + index + " ");
var leftCell = (uObj.hasOwnProperty(index + "")) ? uObj[index + ""] : "----";
var rightCell = (cObj.hasOwnProperty(index + "")) ? cObj[index + ""] : "----";
// Update from Gerard Wijnands in the ServiceNow Community Forum
if (!searchObj(cObj,leftCell)) {
leftCell="<span style='background-color: Coral'>"+leftCell+"</span>";
}
if (!searchObj(uObj,rightCell)) {
rightCell="<span style='background-color: Chartreuse'>"+rightCell+"</span>";
}
params.push(" " + leftCell + " ");
params.push(" " + rightCell + " ");
var row = gs.getMessage(rowTemplate, params);
return row;
};
o.createTable = function(header, rows) {
var tableTemplate = "<table>{0}{1}</table>";
var params = [];
params.push(header);
params.push(rows);
var table = gs.getMessage(tableTemplate, params);
return table;
};
o.createHeader = function(cName, uName, index) {
var rowTemplate = "<thead><tr><th>{0}</th><th>{1}</th><th>{2}</th></tr></thead>";
var params = [];
params.push(" " + index + " ");
params.push(" " + uName + " ");
params.push(" " + cName + " ");
var row = gs.getMessage(rowTemplate, params);
return row;
};
o.messageTemplate = messageTemplate = "<pre>{0}</pre>";
o.showMessage = function(obj) {
var objString = JSON.stringify(obj, undefined, 2);
gs.addInfoMessage(gs.getMessage(this.messageTemplate, objString));
};
var currentXml = o.getRecXml(gr.getValue("file_name"), "current");
var upgradeXMl = o.getRecXml(gr.getValue("file_name"), "history", historyId);
var currentObj = o.getObj(currentXml);
// o.showMessage(currentObj);
var upgradeObj = o.getObj(upgradeXMl);
// o.showMessage(upgradeObj);
var length = Object.keys(currentObj).length;
length = (length > Object.keys(upgradeObj).length) ? length : Object.keys(upgradeObj).length;
var cName = "Our Version";
var uName = "Upgrade Version";
var tHead = "";
tHead = o.createHeader(cName, uName, "position");
var tRows = "";
for (var i = 0; i < length; i++) {
tRows += (o.createRow(currentObj, upgradeObj, i));
}
var table = o.createTable(tHead, tRows);
var winContent = '<textarea>[code]{0}[/code]</textarea>';
gs.addInfoMessage(gs.getMessage(winContent, table));
gs.addInfoMessage(gs.getMessage(table));
} catch (e) {
// enter what you want to happen if there is an error here
var errorMessage = functionName + e;
gs.addInfoMessage(errorMessage);
gs.log(errorMessage);
}
}
// Update from Gerard Wijnands in the ServiceNow Community Forum
function searchObj (obj, query) {
for (var key in obj) {
var value = obj[key];
if (value === query) {
return true;
}
}
return false;
}
myCompare();
action.setRedirectURL(current);

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
This is super helpful, and also useful for comparing Choice Lists.
One caveat however for Choice Lists, is that this code does not differentiate the same choice with different Dependent values.

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Great work cody. Love this!!

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Here is an updated version that also considers dependent values for sys_choice lists:
/*_________________________________________________________________
* Description:
* Parameters:
* Returns:
* Sets:
________________________________________________________________*/
function myCompare() { // replace myCompare with the name fo your function
if (typeof scriptName == "undefined") {
// we will add the scriptName to the function Name
scriptName = "";
}
var functionName = "Function: myCompare\n";
functionName = scriptName + functionName;
try {
// Put content here
var upgradeId = current.getUniqueValue();
var hlSysId = upgradeId; //this.getParameter("sysparm_history_log");
var gr = new GlideRecord("sys_upgrade_history_log");
gr.addQuery("sys_id", hlSysId);
gr.query();
if (!gr.next()) {
return;
}
// Secured.
if (!gr.canRead()) {
gs.logWarning("Security restricted: cannot read", "DiffAjax.diffHistoryLogToCurrentVersion");
return;
}
var historyId = gr.getValue("upgrade_history");
var o = {};
o.getRelationship = function(rel) {
var id = rel.replace(/REL:/, "");
var gr = new GlideRecord("sys_relationship");
if (gr.get(id)) {
return gr.getValue("name");
} else {
return rel;
}
};
o.getRecXml = function(name, state, source) {
if (gs.nil(name) || gs.nil(state)) {
return null;
}
var gr = new GlideRecord("sys_update_version");
gr.addQuery("name", name);
gr.addQuery("state", state);
if (!gs.nil(source)) {
gr.addQuery("source", historyId);
}
gr.orderByDesc("sys_updated_on");
gr.query();
gr.next();
var xml = GlideappUpdateVersion.replacePayloadWithPreviewXML(gr);
return xml;
};
o.getObj = function(record) {
//gs.addInfoMessage(record.payload);
var recordObj = new XMLHelper(record.payload).toObject();
if (recordObj.hasOwnProperty("sys_ui_related_list_entry")) {
return this.getRelatedListObj(recordObj);
} else if (recordObj.hasOwnProperty("sys_ui_element")) {
return this.getFormObj(recordObj);
} else if (recordObj.hasOwnProperty("sys_ui_list_element")) {
return this.getListObj(recordObj);
} else if (recordObj.hasOwnProperty("sys_ui_form_section")) {
return this.getFormSection(recordObj);
} else if (recordObj.hasOwnProperty("sys_choice")) {
return this.getChoices(recordObj);
} else {
this.showMessage(recordObj);
}
};
o.getChoices = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_choice;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = i;
var entryName = "";
if (entry.dependent_value) // Also consider sys_choice dependent values, if they exist.
entryName = entry.label + "-" + entry.value + "-" + entry.dependent_value + "";
else
entryName = entry.label + "-" + entry.value + "";
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getFormSection = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_ui_form_section;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = entry.position;
var entryName = entry.sys_ui_section["@display_value"];
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getListObj = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_ui_list_element;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = entry.position;
var entryName = entry.element;
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getFormObj = function(recordObj) {
var entriesObj = {};
var entries = recordObj.sys_ui_element;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var position = entry.position;
var entryName = entry.element;
entriesObj[position] = entryName;
}
return entriesObj;
};
o.getRelatedListObj = function(recordObj) {
var entriesObj = {};
var listEntries = recordObj.sys_ui_related_list_entry;
for (var i = 0; i < listEntries.length; i++) {
var entry = listEntries[i];
var position = entry.position;
var entryName = this.getRelationship(entry.related_list);
entriesObj[position] = entryName;
}
return entriesObj;
};
o.createRow = function(cObj, uObj, index) {
var rowTemplate = "<tbody><tr><td>{0}</td><td>{1}</td><td>{2}</td></tr></tbody>";
var params = [];
params.push(" " + index + " ");
var leftCell = (uObj.hasOwnProperty(index + "")) ? uObj[index + ""] : "----";
var rightCell = (cObj.hasOwnProperty(index + "")) ? cObj[index + ""] : "----";
// Update from Gerard Wijnands in the ServiceNow Community Forum
if (!searchObj(cObj,leftCell)) {
leftCell="<span style='background-color: Coral'>"+leftCell+"</span>";
}
if (!searchObj(uObj,rightCell)) {
rightCell="<span style='background-color: Chartreuse'>"+rightCell+"</span>";
}
params.push(" " + leftCell + " ");
params.push(" " + rightCell + " ");
var row = gs.getMessage(rowTemplate, params);
return row;
};
o.createTable = function(header, rows) {
var tableTemplate = "<table>{0}{1}</table>";
var params = [];
params.push(header);
params.push(rows);
var table = gs.getMessage(tableTemplate, params);
return table;
};
o.createHeader = function(cName, uName, index) {
var rowTemplate = "<thead><tr><th>{0}</th><th>{1}</th><th>{2}</th></tr></thead>";
var params = [];
params.push(" " + index + " ");
params.push(" " + uName + " ");
params.push(" " + cName + " ");
var row = gs.getMessage(rowTemplate, params);
return row;
};
o.messageTemplate = messageTemplate = "<pre>{0}</pre>";
o.showMessage = function(obj) {
var objString = JSON.stringify(obj, undefined, 2);
gs.addInfoMessage(gs.getMessage(this.messageTemplate, objString));
};
var currentXml = o.getRecXml(gr.getValue("file_name"), "current");
var upgradeXMl = o.getRecXml(gr.getValue("file_name"), "history", historyId);
var currentObj = o.getObj(currentXml);
// o.showMessage(currentObj);
var upgradeObj = o.getObj(upgradeXMl);
// o.showMessage(upgradeObj);
var length = Object.keys(currentObj).length;
length = (length > Object.keys(upgradeObj).length) ? length : Object.keys(upgradeObj).length;
var cName = "Our Version";
var uName = "Upgrade Version";
var tHead = "";
tHead = o.createHeader(cName, uName, "position");
var tRows = "";
for (var i = 0; i < length; i++) {
tRows += (o.createRow(currentObj, upgradeObj, i));
}
var table = o.createTable(tHead, tRows);
var winContent = '<textarea>[code]{0}[/code]</textarea>';
gs.addInfoMessage(gs.getMessage(winContent, table));
gs.addInfoMessage(gs.getMessage(table));
} catch (e) {
// enter what you want to happen if there is an error here
var errorMessage = functionName + e;
gs.addInfoMessage(errorMessage);
gs.log(errorMessage);
}
}
// Update from Gerard Wijnands in the ServiceNow Community Forum
function searchObj (obj, query) {
for (var key in obj) {
var value = obj[key];
if (value === query) {
return true;
}
}
return false;
}
myCompare();
action.setRedirectURL(current);