
- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on 05-24-2020 07:44 PM
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
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)
|
|
|
The result:
An overview of the Table and it's functions
Field name | Description |
Name | A descriptive name for the Message |
Active | Only active messages will be displayed. |
Table | This 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:
Changes the colour of the alert box that appears on the form. |
When to trigger |
Available options are:
|
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.
- 3,347 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
will this help show message on the a requets or Incident that its already breached or like elasped time has passed % of time

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.