codycotulla
Tera Guru

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.

find_real_file.png

 

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.

find_real_file.png

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("&nbsp;" + index + "&nbsp;");
            var leftCell = (uObj.hasOwnProperty(index + "")) ? uObj[index + ""] : "----";
            var rightCell = (cObj.hasOwnProperty(index + "")) ? cObj[index + ""] : "----";
            params.push("&nbsp;" + leftCell + "&nbsp;");
            params.push("&nbsp;" + rightCell + "&nbsp;");
            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("&nbsp;" + index + "&nbsp;");
            params.push("&nbsp;" + uName + "&nbsp;");
            params.push("&nbsp;" + cName + "&nbsp;");
            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.

find_real_file.png

Let me know if you have any questions.

Thanks,

Cody

Comments
Gerard Wijnand1
Kilo Guru

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

Gerard Wijnand1
Kilo Guru

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("&nbsp;" + index + "&nbsp;");
            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("&nbsp;" + leftCell + "&nbsp;");
            params.push("&nbsp;" + rightCell + "&nbsp;");
            var row = gs.getMessage(rowTemplate, params);
            return row;
        };

In this way the differences are more visible:

find_real_file.png

 

codycotulla
Tera Guru

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("&nbsp;" + index + "&nbsp;");
            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("&nbsp;" + leftCell + "&nbsp;");
            params.push("&nbsp;" + rightCell + "&nbsp;");
            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("&nbsp;" + index + "&nbsp;");
            params.push("&nbsp;" + uName + "&nbsp;");
            params.push("&nbsp;" + cName + "&nbsp;");
            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);
Matthew Frazier
Tera Contributor

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.

MrMuhammad
Giga Sage

Great work cody. Love this!!

Matthew Frazier
Tera Contributor

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("&nbsp;" + index + "&nbsp;");
            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("&nbsp;" + leftCell + "&nbsp;");
            params.push("&nbsp;" + rightCell + "&nbsp;");
            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("&nbsp;" + index + "&nbsp;");
            params.push("&nbsp;" + uName + "&nbsp;");
            params.push("&nbsp;" + cName + "&nbsp;");
            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);
Version history
Last update:
‎09-16-2020 08:37 AM
Updated by: