Luke Van Epen
Tera Guru

Introducing Popup Manager, a way to write Info and Error messages without a single line of code

Foreword:

This article is an in depth explanation into the problem statement and how I went about solving the issue.

If you just want to download the complete app and start writing (or delegating the writing of) messages without any code, you can visit my newly uploaded Share package here: https://developer.servicenow.com/connect.do#!/share/contents/7563463_popup_manager?t=PRODUCT_DETAILS

Contributions welcome

Where are we now?

Popups seem like one of those things that really shouldn't require a System Admin to create. All you're doing is evaluating the state of a record, and showing some text at the top of the screen or under a field. Yet, to achieve this basic outcome, we need to write code.

This can become a problem for large environments with teams who want to utilise this feature as an augment to their process, big case of course is the Service Desk. There's hardly anyone who wouldn't benefit from contextual popups guiding them through ticket creation and actioning.

The goal

What if we could stop System Admins from ever needing to write g_form.addInfoMessage ever again? What if we could empower Tier 1 Agents to write hundreds of individual popup messages with no performance impact? What if we could remove the need for the Service Desk to constantly search for knowledge articles for every ticket they get?

Outcomes

- We want a No-Code way to display Info, Warning and Error messages on any Form.

- We need to be able to show the message based on the conditions of the record, and conditionally based on the user viewing the record.

- We need to support OnLoad and OnChange at a minimum.

- We must ensure it is a scalable feature with no percieved performance impact (Asynchronous Everything)

- It needs to be easy enough for a Service Desk agent to use.

- We want support for Rich HTML content such as hyperlinks, and tables in Info Messages.

Some nice to haves:

- Field styling (This one requires direct DOM manipulation, so understandable if you dont want to include it, but you get to colour in your form and put pictures everywhere so its a good visual aid for agents).

 

The anatomy of addInfoMessage, addErrorMessage, showFieldMsg, showErrorBox

find_real_file.png

The OOB methods on g_form appear quite simple, but when breaking it down into discrete components, there's actually a lot going on we need to capture.

Normally one of these methods in a client script is preceded by a condition. E.g. "If the CI is this, then show this message". So we need a way to capture form conditions.

What if we only want to show messages for specific users? Maybe the Service Desk should get special popups but Level 2 and 3 don't need the clutter. We need to capture user conditions as well.

Is it an Info message or Error message? Here's another field. Do we want to show it at the top of the form as a banner, or under a field to be more localised? Another field.

 

How do we do it?

This mini-application will involve a few components:

1. A new table to store the Popup Messages

2. A bunch of Client Scripts (1 onLoad per table, plus 1 onChange per field we want to watch)

3. An Ajax Script include

4. ACLs, System Properties, App menus and modules.

 

The Table

Some preview Images to get an idea of our finished product. (click to zoom in)

find_real_file.png

find_real_file.png

 

find_real_file.png

find_real_file.png 

 

 

 

The result:

 find_real_file.png

 

 An overview of the Table and it's functions

 

 

Field nameDescription
NameA descriptive name for the Message
ActiveOnly active messages will be displayed.
TableThis message will appear for the selected table. Drives other fields on the form, such as the Condition, Display Field and so on.
Condition

Enter in a condition to control when the message should display. Use the Preview button to see a list of existing records that match the condition you have entered.

User condition
Enter a condition to control which users the message should display for. You can use Related List Conditions to add filters for related records such as Groups and Roles.
Display location
This is effectively the difference between g_form.addInfoMessage and g_form.showFieldMsg. "Top of form" triggers addInfoMessage, and "Below a field" triggers showFieldMsg
Display field
Shows up when Below a field is selected in Display Location.
Is used to position the Message content below a specific field, handy for things that are reactive to a specific field changing and you need to alert the user to something in this location.
Display type
Available types are:
  • Information
  • Warning (Only for "Top of form" messages)
  • Error

Changes the colour of the alert box that appears on the form.

When to trigger
Available options are:
  • When the form loads (I.e. onLoad only)
  • When anything changes on the form (i.e. onLoad is ignored, but after the user starts to make changes to the form, if the condition matches, the Message will be displayed)
  • When a specific field changes (i.e. onChange for a specific field. Even if the form matches the Condition of the message, the contents will only display if the specified field is actively changed by the User)
  • When the form loads or changes (This is the simplest option, will display the message whenever the form matches the Condition)
Field to watch
Only available if "When a specific field changes" is selected in the "When to trigger" field. This denotes which field needs to be changed by the user in order to activate the rule and display the message. This is equivalent to the "Field" field on a standard onChange client script
Available options are dependent on which Table is selected.
Reverse on false condition
This is used to stop the message from displaying if the Condition no longer matches. This uses both "When to trigger" as well as the Condition field to determine whether the message should be reversed.
As a result, combining "Reverse on false condition" with a "When a specific field changes" trigger will cause the message to be undone whenever any field other than the watched field is changed by the user, so this is usually not recommended.
Message
Free-text content. You can use ${field_name} or ${field_name.dot_walked_field} syntax to grab dynamic values from the form. You can also dot-walk on reference fields.
Message (HTML)
HTML content, only displays if "Top of the form" display location is selected, because only g_form.addInfoMessage supports rich HTML content such as links and text styling.
However, this can allow you to add really complex HTML styled info messages with ease, since you have the full power of the HTML editor to help you build your message.
Enable field styling
When ticked, displays the Field styling section and allows field styles to be manipulated by messages, allowing even more dynamic and impactful popup messages.
Styled field
The selected field will be the target of the styling options
Text colour
Changes the text colour of the Styled field to the selected colour. Standard CSS named colours are supported, as well as Hex values. This field is a standard ServiceNow Colour picker field.
Background colour
Changes the background colour of the Styled field to the selected colour
Decoration icon
Uses g_form.addDecoration to add the selected icon to the Styled field
Decoration colour
Styles the decoration with the specified colour. No effect if the decoration icon is empty
Preview
This field is present on the form simply to show you how the Field Styling will look, so that you don't have to trigger the message in order to test how it looks.

 

 

 

Client Scripts (PopupLoader)

The previous sections are all you need to get started with using the application, however if you would like to understand how it all works, I'll detail the code below.

All the client scripts are exactly the same, but one is needed per base table simply because Scoped Applications are not allowed to run Client Scripts in Global.

To add support for your own custom tables (task based tables should already be included by one of the provided Client Scripts), there are a few steps:

  • Table - to your table
  • Inherited - true if your table has child tables that need to load the popup message as well
  • Isolate Script - Set to False this is important, otherwise the direct DOM manipulation parts of the Styling will not be applied. You need to save the record before you can set Isolate Script to false. It may not be present on the form in your instance, you might have to go to a list view of the client scripts and set it to false from there.

Why not UI Scripts? UI Scripts have 1 major problem - they are cached client side. This makes it extremely difficult and unreliable to trigger an update for all Clients when the UI Script changes, meaning any patches/bug fixes can be very difficult to roll out to the user base.

The code (explanations below)

function onLoad() {
    if (typeof g_form === "undefined")
        return;


    g_form.elements.forEach(function(element) {
        g_event_handlers.push(new GlideEventHandler('onChange_PopupMessageLoader.' + element.fieldName, popupChangeHandler, g_form.getTableName() + '.' + element.fieldName));
    });

    function popupChangeHandler(control, oldValue, newValue, isLoading, isTemplate) {
        if (isLoading)
            return;
        var fieldName = control.name.split('.')[1];
        var changedFields = [];
        g_form.elements.forEach(function(element) {
            var name = element.fieldName;
            var e = g_form.getElement(name);
			if (e && g_form.changedFieldsFilter(e) == true) {
                changedFields.push({
                    name: name,
                    value: g_form.getValue(name),
                    thisUpdate: name == fieldName
                });
            }
        });

        // checks if the changedFields array contains the most recently changed field
        // and adds it if not
        if (!changedFields.some(function(object) {
                return object.name == fieldName;
            })) {
            changedFields.push({
                name: fieldName,
                value: g_form.getValue(fieldName),
                thisUpdate: true
            });
        }


        var ga = new GlideAjax("PopupMessageAjax");
        ga.addParam("sysparm_name", "showMessages");
        ga.addParam("sysparm_id", g_form.getUniqueValue());
        ga.addParam("sysparm_table", g_form.getTableName());
        ga.addParam("sysparm_user", g_user.userID);
        ga.addParam("sysparm_change", "true");
        ga.addParam("sysparm_changed_fields", JSON.stringify(changedFields));

        ga.getXMLAnswer(displayMessages);
    }

    if (g_form.isNewRecord()) {
        return; //onLoad wont run on new records.
    }

    var ga = new GlideAjax("PopupMessageAjax");
    ga.addParam("sysparm_name", "showMessages");
    ga.addParam("sysparm_id", g_form.getUniqueValue());
    ga.addParam("sysparm_table", g_form.getTableName());
    ga.addParam("sysparm_user", g_user.userID);
    ga.addParam("sysparm_change", "false");
    ga.getXMLAnswer(displayMessages);

    function displayMessages(answer) {
        /**
         * Housekeeping steps:
         * 1. If the message has already been loaded, ignore the message
         * 2. If an already loaded message is not in the new array, reverse it because it no longer applies
         * 3. Apply any new messages 
         */
        if (!g_scratchpad.popup_messages) {
            g_scratchpad.popup_messages = [];
        }


        var messageArray = JSON.parse(answer);
        if (!Array.isArray(messageArray) || messageArray === null || messageArray.length == 0) {
            g_scratchpad.popup_messages.forEach(function(loadedMessage) {
                reversePopup(loadedMessage);
            });
            return;
        }

        var returnedMessageIds = [];


        messageArray.forEach(function(message) {
            if (message == null || message == undefined || message == "" || message == {} || message == [])
                return;


            returnedMessageIds.push(message.message_id);

            var skipLoad = false;
            if (g_scratchpad.popup_messages.length > 0) {
                g_scratchpad.popup_messages.forEach(function(loadedMsg) {
                    if (loadedMsg.message_id == message.message_id) {
                        if (loadedMsg.message == message.message)
                            skipLoad = true;
                        // If the msg we just got has already been loaded into the scratchpad, 
                        // do not try to apply it twice, or popup boxes will duplicate
                    }
                });
            }
            if (skipLoad) {
                return;
            }

            var scratchpadObject = message;

            var tableName = g_form.getTableName();

            //Top of screen
            if (message.location == "top" && message.display_type == "Informational") {
                g_form.addInfoMessage(message.message);
            } else if (message.location == "top" && message.display_type == "Warning") {
                g_form.addWarningMessage(message.message);
            } else if (message.location == "top" && message.display_type == "Error") {
                g_form.addErrorMessage(message.message);
            }
            //Under a field
            else if (message.location == "field" && message.display_type == "Informational") {
                g_form.showFieldMsg(message.field, message.message, "info");
            } else if (message.location == "field" && message.display_type == "Error") {
                g_form.showFieldMsg(message.field, message.message, "error");
            }
            //Field styling
            if (message.styles_enabled == true) {
                var fieldName = message.style_field;
                var fieldElement;
                if (g_form.isReadOnly(null, g_form.getElement(fieldName))) {
                    fieldElement = g_form.getElement('sys_readonly.' + tableName + "." + fieldName) ||
                        g_form.getElement(tableName + "." + fieldName); //if it's a choice field the first part wont work
                } else if (g_form.getControl('sys_display.' + tableName + '.' + fieldName) != undefined) {
                    fieldElement = g_form.getElement('sys_display.' + tableName + '.' + fieldName);
                } else {
                    fieldElement = g_form.getElement(fieldName);
                }
				if(!fieldElement)
					return; // field is not on the form, so no style to apply

                var labelElement = g_form.getLabel(fieldName);
                labelElement.style.backgroundImage = "";
                if (message.text_colour != null)
                    fieldElement.style.color = message.text_colour;
                if (message.background != null)
                    fieldElement.style.background = message.background;
                if (message.icon != null) {
                    g_form.addDecoration(fieldName, "icon-" + message.icon, "", message.icon_colour);

                }
            }

            if (!g_scratchpad.popup_messages.some(function(message) {
                    return message.message_id == scratchpadObject.message_id;
                })) {
                g_scratchpad.popup_messages.push(scratchpadObject); //debugging
            }
        });

        g_scratchpad.popup_messages.forEach(function(loadedMessage) {
            if (returnedMessageIds.indexOf(loadedMessage.message_id) == -1) {
                // Message no longer applies
                reversePopup(loadedMessage);
            }
        });

        function reversePopup(message) {
            var tableName = g_form.getTableName();

            if (!message.reverse_on_false)
                return; // do not reverse persistent messages

            //Top of screen
            if (message.location == "top") {
                g_form.clearMessages();
            }
            //Under a field
            else if (message.location == "field") {
                g_form.hideFieldMsg(message.field);
            }
            //Field styling
            if (message.styles_enabled == true) {
                var fieldName = message.style_field;
                var fieldElement;
                if (g_form.isReadOnly(null, g_form.getElement(fieldName))) {
                    fieldElement = g_form.getElement('sys_readonly.' + tableName + "." + fieldName) ||
                        g_form.getElement(tableName + "." + fieldName); //if it's a choice field the first part wont work
                } else if (g_form.getControl('sys_display.' + tableName + '.' + fieldName) != undefined) {
                    fieldElement = g_form.getElement('sys_display.' + tableName + '.' + fieldName);
                } else {
                    fieldElement = g_form.getElement(fieldName);
                }

                var labelElement = g_form.getLabel(fieldName);
                labelElement.style.backgroundImage = "";
                if (message.text_colour != null)
                    fieldElement.style.color = "";
                if (message.background != null)
                    fieldElement.style.background = "";
                if (message.icon != null) {
                    g_form.removeAllDecorations();
                }
            }

            g_scratchpad.popup_messages.forEach(function(loadedMsg, idx) {
                if (loadedMsg.message_id == message.message_id) {
                    g_scratchpad.popup_messages.splice(idx, 1); // remove that message from the scratchpad so it can be reapplied
                }
            });
        }
    }
}

 

g_event_handlers and the GlideEventHandler

The magic happens here, the GlideEventHandler is a constructor function which is what ServiceNow uses under the hood to convert a Client Script into an onChange event handler for a specified field using native DOM functionality.

By tapping into this function, we can register our own onChange even handlers at run time during the onLoad script.

GlideEventHandler takes 3 arguments:

  • a display name for the handler
  • a callback function to handle the change event
  • the field to attach the event to in the format "table_name.field_name"

The when registered this way, the callback function is given 5 handy parameters:

  • control
  • oldValue
  • newValue
  • isLoading
  • isTemplate

If these look familiar, that's because they're the exact same parameters as in an OOB onChange Client Script. This is how we can substitute needing to create 100s of onChange client scripts and instead dynamically attach the same event handler script to each field on the form.

 

popupChangeHandler

Thanks to the above GlideEventHandler functionality, we can now create our onChange script within the onLoad script and attach this as the callback to our event handler. This is then made table-agnostic by utilising the native g_form.getTableName method to detect what table we're on.

This handler then loops through all the visible fields on the form, and checks if they have changed, adding each field which has been modified since form load to an array which is passed up to the Ajax script. More on the Ajax functionality below.

displayMessages

Once the server has lookup up the messages and told us which ones to load, this function takes care of managing the client side functions that display the messages and attach styling to the form.

g_scratchpad is used to keep track of messages which have already been loaded, so that subsequent calls to the Ajax script do not result in the same message appearing multiple times on the form.

reversePopup

In the event that a popup message should be removed from the form, then this function takes care of undoing the alerts and field styling applied for that particular message. This only runs if the Message has the "Reverse on false condition" checkbox ticked.

 

Ajax Script Include

Code (explanations below:

 

var PopupMessageAjax = Class.create();
PopupMessageAjax.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    appEnabled: gs.getProperty('x_a3_popup_msg_v2.app.enabled') == "true",

    POPUP_MESSAGE_TABLE: "x_a3_popup_msg_v2_message",
    logStart: new Date().getTime(),
    taskValid: gs.getProperty('x_a3_popup_msg_v2.valid.tables').split(',').indexOf('task') >= 0,

    mode: "load",
    changed_fields: [],
    active_field: "",
    log_cache: [],

    //simple logger
    _log: function(msg) {
        if (gs.getProperty('x_a3_popup_msg_v2.enable.logging') == 'true') {
            var logTime = new Date().getTime();
            var stamp = logTime - this.logStart;
            stamp = ("0000" + stamp).slice(-4);
            // gs.info('[' + stamp + '] ' + msg);
            this.log_cache.push('[' + stamp + '] ' + msg);
        }
    },

    getFieldReference: function() {
        var table = this.getParameter("sysparm_table");

        var fieldNames = new GlideRecord('sys_dictionary');
        fieldNames.addQuery('name', table);
        fieldNames.addNotNullQuery('element');
        fieldNames.query();
        var fieldArray = [];
        while (fieldNames.next()) {
            var obj = {
                name: fieldNames.getValue('element'),
                label: fieldNames.getValue('column_label')
            };
            fieldArray.push(obj);
        }

        return JSON.stringify(fieldArray);
    },

    /**SNDOC
    @name showMessages
    @description returns an array of Info or Error messages to display, by looking up the Popup Messages table for matching tickets

    @param {string} [sysparm_id] - the id of the Ticket that the onLoad client script is calling from
    @param {string} [sysparm_table] - the table name that the onLoad client script is calling from
    @param {string} [sysparm_user] - the id of the User who triggered the script
    @param {string} [sysparm_change] - true if calling from an onChange event, false if calling from onLoad.
    @param {string} [sysparm_changed_fields] - JSON string of an array of changed field names and values in the format [{name:'category',value:'software'},{'name':'subcategory',value:'outlook'}];

    @returns {Array} messages to be rendered by the client script
    */
    showMessages: function() {
        this._log("Running Popup Message Manager");

        if (!this.appEnabled) {
            this._log("Aborting Popup Message Manager due to app being disabled in properties");
            return null;
        }
        var ticketID = this.getParameter('sysparm_id');
        var ticketTable = this.getParameter('sysparm_table');
        var userID = this.getParameter('sysparm_user');
        var onChange = this.getParameter("sysparm_change");

        var userGR = new GlideRecord('sys_user');
        userGR.get(userID);
        this._log("User: " + userGR.getDisplayValue())

        var ticketGR = new GlideRecordSecure(ticketTable); // ensures that field parsing does not expose data to users who are not permitted to see it
        ticketGR.get(ticketID);

        if (onChange == "true") {
            this.mode = "change";
            var changedFields = JSON.parse(this.getParameter("sysparm_changed_fields"));
            var self = this;
            //             this._log("Debug: " + JSON.stringify(changedFields, null, 2));
            changedFields.forEach(function(field) {
                self.changed_fields.push(field.name);
                if (field.thisUpdate)
                    self.active_field = field.name;
                ticketGR.setValue(field.name, field.value);
            });
        } else {
            this.mode = "load";
        }

        this._log("Mode " + this.mode + " active field: " + this.active_field + " changed field list " + this.changed_fields.toString());
        return this._run(ticketGR, ticketTable, userGR);

    },

    /**SNDOC
    @name _run
    @private
    @description runs the main computation

    @param {GlideRecord} [ticketGR] - the ticket that we are checking against the Popup Messages table. This could have been modified to suit onChange client scripts
    @param {string} [ticketTable] - the name of the table to run the check against
    @param {GlideRecord} [userGR] - the user who triggered the script.

    @returns {Array} the message array to be sent back to the client via showMessages or showMessagesOnChange
    */
    _run: function(ticketGR, ticketTable, userGR) {
        var messages = new GlideRecord(this.POPUP_MESSAGE_TABLE);
        messages.addActiveQuery();
        this._log("Task valid: " + this.taskValid);
        if (this.taskValid) {
            messages.addQuery('message_table', 'IN', 'task,' + ticketTable);
        } else {
            messages.addQuery('message_table', ticketTable);
        }

        if (this.mode == "load") {
            messages.addQuery("message_when", "IN", "on_load,on_load_or_change");
        } else if (this.mode == "change") {
            messages.addQuery("message_when", "IN", "on_form_change,on_field_change,on_load_or_change");
        }

        this._log("Encoded query: " + messages.getEncodedQuery());
        messages.query();
        var messageArray = [];
        while (messages.next()) {

            var condition = messages.getValue('table_condition');

            var userCondition = messages.getValue('user_condition');

            this._log("Checking User" + userGR.getDisplayValue() + "\nAgainst condition: \n" + userCondition);
            if (userCondition != null && !GlideFilter.checkRecord(userGR, userCondition)) {
                this._log("User check failed");
                continue; //User does not match the criteria for displaying the popup
            } else {
                this._log("User check passed");
            }

            this._log("Checking Record" + ticketGR.getDisplayValue() + "\nAgainst condition: \n" + condition);

            if (condition != null && GlideFilter.checkRecord(ticketGR, condition)) {
                this._log("Record check initially passed for " + ticketGR.getDisplayValue() + " against rule " + messages.getDisplayValue());
                var when = messages.getValue("message_when");
                this._log("Message when is: " + when);
                if (when == "on_field_change") {
                    if (this.mode == "change") {
                        if (messages.message_reverse_on_false) {
                            if (this.active_field == messages.getValue("message_watched_field")) {
                                var responseObj = this._getResponseDetails(messages, ticketGR);
                                this._log("Pushing response object: " + JSON.stringify(responseObj, null, 2));
                                messageArray.push(responseObj);
                            } else {
                                this._log("Record check failed because the most recently changed field: " + this.active_field + " was not the watched field in the rule: " + messages.getValue("message_watched_field"));
                            }
                        } else {
                            if (this.changed_fields.indexOf(messages.getValue("message_watched_field")) >= 0) {
                                var responseObj = this._getResponseDetails(messages, ticketGR);
                                this._log("Pushing response object: " + JSON.stringify(responseObj, null, 2));
                                messageArray.push(responseObj);
                            } else {
                                this._log("Record check failed because the watched field is not in the changed list: " + this.changed_fields.toString() + " : " + messages.getValue("message_watched_field"));
                            }
                        }
                    } else {
                        this._log("Record check failed because the mode is onLoad and this rule is only for field changes");
                    }
                } else if (when == "on_form_change") {
                    if (this.mode == "change") {
                        var responseObj = this._getResponseDetails(messages, ticketGR);
                        this._log("Pushing response object: " + JSON.stringify(responseObj, null, 2));
                        messageArray.push(responseObj);
                    } else {
                        this._log("Record check failed because the mode is onLoad and this rule is only for field changes");
                    }
                } else if (when == "on_load") {
                    this._log("Inside when==on_load, this.mode = " + this.mode);
                    if (this.mode == "load") {
                        var responseObj = this._getResponseDetails(messages, ticketGR);
                        this._log("Pushing response object: " + JSON.stringify(responseObj, null, 2));
                        messageArray.push(responseObj);
                    } else {
                        this._log("Record check failed because the mode is onLoad and this rule is only for field changes");
                    }
                } else if (when == "on_load_or_change") {
                    var responseObj = this._getResponseDetails(messages, ticketGR);
                    this._log("Pushing response object: " + JSON.stringify(responseObj, null, 2));
                    messageArray.push(responseObj);
                }
            } else {
                this._log("Record check failed for " + ticketGR.getDisplayValue() + " against rule " + messages.getDisplayValue());
                continue;
            }
        }
        // {message:'string/html',location:'top/field',field:'null/field_name',display_type:'Informational/Error'}
        this._log("Returning array: \n" + JSON.stringify(messageArray, null, 2));
        gs.info(this.log_cache.join("\n"));
        return JSON.stringify(messageArray);
    },

    /**SNDOC
    @name _getResponseDetails
    @private
    @description Once we know that the record matches a popup message condition, grab the relevant details for sending back to the client

    @param {GlideRecord} [messages] - the GR for the Popup message that will be used to retreive values

    @returns {Object} an object containing the relevant fields to be parsed by the client script: 
    {
        display_type: 'string',
        message:'string',
        location:'string',
        field:'string|null'
    }
    */
    _getResponseDetails: function(messages, ticketGR) {
        this._log("Getting response details for " + messages.getDisplayValue() + " and ticket " + ticketGR.getDisplayValue());
        try {

            var displayArea = messages.getValue('message_location');
            var displayType = messages.getValue('message_type');
            var enableStyles = messages.getValue('style_enabled');
            var reverseOnFalse = messages.getValue("message_reverse_on_false");

            var responseObj = {};

            // Controls whether the message should disappear if the form changes
            responseObj.reverse_on_false = reverseOnFalse == "1" ? true : false;

            // Handle field styling information
            if (enableStyles == "1") {
                responseObj.styles_enabled = true;
                responseObj.style_field = messages.getValue('style_field');
                responseObj.text_colour = messages.getValue('style_text_colour') || null;
                responseObj.background = messages.getValue('style_background_colour') || null;
                responseObj.icon = messages.getValue("style_decoration") || null;
                responseObj.icon_colour = messages.getValue("style_decoration_colour") || null;
            } else {
                responseObj.styles_enabled = false;
            }

            responseObj.display_type = displayType;
            if (displayArea == "top") {
                responseObj.message = this._parseMessageField(messages.getValue("message_html"), ticketGR);
                responseObj.location = "top";
                responseObj.field = null;
            } else if (displayArea == "field") {
                responseObj.message = this._parseMessageField(messages.getValue("message_plain"), ticketGR);
                responseObj.location = "field";
                responseObj.field = messages.getValue("message_field");
            }

            responseObj.message_id = messages.getUniqueValue(); //for use in g_scratchpad
        } catch (e) {
            this._log("Error getting responseObj " + e);
        }

        this._log("Message Response object details: \n" + JSON.stringify(responseObj, null, 2));
        return responseObj;
    },

    _parseMessageField: function(messageString, ticketGR) {
        this._log("Attempting to parse messageString: " + messageString);

        function resolve(path, obj) {
            return path.split('.').reduce(function(prev, curr) {
                return prev ? prev[curr] : null;
            }, obj || self);
        }

        var newMessage = messageString;
        var re = /\${([^}]+)}/g;
        var result;
        while ((result = re.exec(messageString)) !== null) {
            try {

                var fieldName = result[1];
                var fieldValue = resolve(fieldName, ticketGR).getDisplayValue();
                newMessage = newMessage.replace(result[0], fieldValue);
            } catch (e) {
                this._log("Error while parsing " + e + [fieldName, fieldValue, result].join('\n'));
            }
        }


        return newMessage;

    },

    type: 'PopupMessageAjax'
});

 

Key points

Firstly, we get the GlideRecord of the form that is being checked, then we modify it in-flight using the changed fields array sent by the client, before checking it against the conditions in the Popup Message table. GlideFilter.checkRecord is used to compare an encoded query with a GlideRecord, and return true if it matches.

getResponseDetails

This helper function is used to convert the GlideRecord of the Popup Message into a simple Object that can be used client side and stored in the scratchpad

parseMessageField

This function is used to parse out the ${field_name} syntax, and obtain the updated GlideRecord's field value of this field. This works while dot-walking, and it also uses the most recent value selected by the User client-side when obtaining the value of the GlideRecord. In this way, the message can be dynamically updated with a value that is obtained from a reference field's dot-walked field, after the user has changed it, and then display this in the message to the user.

 

ACLs, Properties and extra stuff

There's a few things we want to tie off to make this a fully functional app and not just a collection of scripts

Properties:

There are a handful of properties in use to make maintenance of the main application easier. This includes a kill-switch to turn off all scripts with one checkbox, a controlled list of tables which can have messages added to them, a switch for enabling verbose logging.

Access Controls:

The app by default allows itil_admin to do anything with popup messages, but you may want to update this to use a custom role or change it to give free reign to anyone with backend access.

Application menus:

It helps to be able to navigate to all the core parts of the application from one menu. I recommend doing this for any application you create, as it makes things way easier to debug when you can immediately pull up all records related to an application in a couple of clicks.

find_real_file.png

Comments
rajesh9885
Mega Guru

will this help show message on the a requets or Incident that its already breached or like elasped time has passed % of time

Luke Van Epen
Tera Guru

No, it will only allow access to content from the form itself. That kind of message would not be displayed with a Client Script, you would use a Display Business Rule for that.

Version history
Last update:
‎05-24-2020 07:44 PM
Updated by: