Skip navigation

Developer Community

6 Posts authored by: Slava
Slava

The new old search

Posted by Slava Sep 2, 2015

What I particularly like about ServiceNow is that there are plenty of features to discover even after many years of exposure to the product. Not long ago I stumbled upon a global out-of-box UI Action named Search that dated back to February 2008 and strangely contained no script, no condition, no action name – no nothing. The only hint was a comment that read: Search button on our search screens. Search screens? Honestly, I had never heard of them before. Although Wiki makes mention of this feature in Introduction to Searching and Administering Applications and Modules, the information provided is too scarce to catch the readers' attention.

 

In essence, appending _search to the name of any table (e.g., sys_user_search.do) will take you to what looks like a normal form for that table, though with a few important differences:

 

  • No form UI actions (except the Search button)
  • No mandatory fields
  • No read-only fields
  • No default values
  • No hidden fields
  • Dropdowns instead of checkboxes (in order to differentiate false from not selected)

 

In other words, the form that is normally used for inserting, updating or viewing individual records turns into an advanced search form. Instead of scrolling up and down the endless list of fields in the condition builder, all you have to do is fill in a couple of fields on a familiar form, press the Search button and – voila! – you have the filtered list you were after. Isn’t that a good time-saver? Yes, it is, though as usual there are some trade-offs to bear in mind:

 

  • No contains or equals search (starts with is the default and only operator for text fields)
  • You cannot search for an empty value or --None--
  • Due to reference qualifiers, you cannot search for values that are not normally selectable in a given reference field.

 

Nonetheless, the feature seems to be quite handy. From my own experience, there are a number of situations where it really helps:

 

  • When working on a multi-language instance, it is impossible to remember the localized labels for all fields. With the search screen feature, there is no need to first look the label up on the form and then go back to the list to do a search.
  • Sometimes people right nasty client scripts that dynamically change field labels on the form. As a result, trying to find the field in the list filter can drive you crazy. With search screens, however, you do not have to worry about it at all.
  • And last but not least, search screens seem to be the quickest way to explore the form layout as no fields are hidden. Unlike form layout configuration page where you have to switch between form sections, on a search screen you have the entire form at your fingertips.

 

I am sure you will find your own use cases. Do not hesitate to share them in comments!

Slava

Plug-ins ins and outs

Posted by Slava Aug 5, 2015

As I was preparing a demo instance for one of the customers last week, I realized how tedious plug-in activation could be. Activating a single plug-in is a piece of cake. But what about a dozen? Waiting for each process to complete before launching the next one is counter-productive. On the other hand, activating several plug-ins in parallel is risky due to increased performance impact and possible conflicts resulting from plug-in dependencies. Moreover, what if you needed to activate a plug-in at a specific time out of business hours?

 

The obvious answer is scripting, and it did not take me long to find the out-of-box script include that triggers activation of a plug-in when you press Activate button in the dialog window. With the following piece of code, you can launch plug-in activation from any server-side script or even schedule it to take place on a Christmas night without running the risk of missing the family reunion:

 

var worker = new GlidePluginManagerWorker();
worker.setProgressName('plugin_name');        // any descriptive name will do
worker.setPluginId('plugin_id');              // must match the ID of the plugin
worker.setBackground(true);
worker.start();

 

In this example, setPluginId() method tells the system which plug-in to activate while setProgressName() simply assigns a name to the corresponding progress worker. You would typically use the Name of the plug-in here but any descriptive name will do. Should any other plug-ins be required, they will be activated automatically and within the same progress worker.

 

If you are not familiar with progress workers, they are a means of tracking the status of certain types of background processes. When you preview or commit an update set, import data using import sets, or even clone a Service Catalog request, there is a record in sys_progress_worker table that displays the current status of the activity and is constantly updated as the process progresses. Progress workers are available from the System Diagnostics application menu.

 

GlidePluginManagerWorker class also has a couple of optional methods for working with plug-in demo data. Depending on your needs, include one of these statements before worker.start() command:

 

worker.setIncludeDemoData(true);      // load demo data during activation of a plug-in
worker.setLoadDemoDataOnly(true);     // load demo data for a previously activated plug-in

 

And last but not least – there is a method for grabbing the sys_id of the relevant progress worker record.

 

var pw = worker.getProgressID();

 

You can have your script query the progress worker record to determine whether activation has completed and trigger subsequent actions such as activation of the next plug-in, an e-mail notification or closure of the corresponding change request.

 

Now that you are fully equipped, give it a try and share your experience and use cases in the comments.

Slava

Trailing the trailing space

Posted by Slava Jul 22, 2015

Extra spaces are clearly one of those nightmares of the IT world that continually haunt end users as well as admins and developers. While surfing the web, have you ever come across ugly hyperlinks with underlining stretching beyond the link text? Or have you ever been unable to log in to an application because you overlooked a trailing space when copying the password from an email? Or maybe you happened to waste a few hours debugging your app because of an extra whitespace character in the value of a string variable that went unnoticed?

 

Now that these examples have hopefully evoked some reminiscences, let us get down to the business of banishing unwanted spaces from your ServiceNow instances. It goes without saying that the golden rule for any development is of course to cleanse the data before putting it into the database. In ServiceNow, this is easily achievable through a before business rule:

 

current.field_name = current.field_name.trim();

 

It takes almost no time to create a rule that can save you hours of troubleshooting headache later on!

 

Preventive measures are never a bad thing but the question you are likely to ask is: "What if I wanted to check if extra spaces have already found their way into my ServiceNow instance?"

 

Indeed one cannot search for a whitespace character using list search in a regular way because – as you would naturally expect – ServiceNow follows the aforementioned golden rule and trims the input before executing the search. So in this case – strange as it may sound – we will have to work around a best practice. Luckily list search supports the use of JavaScript. Thus if you wanted to find all records in a table with a trailing space in, say, Name field, you could simply select that field in the Go to box and search for:

 

%javascript:' ';

 

Note the usage of the % character as a wildcard in this example. Alternatively you could build the following condition in the list filter to achieve the same result:

 

     Name – ends with – javascript:' ';

 

As you can see, the trick is very simple and leaves no space for undesired spaces in your ServiceNow instance. Moreover, the same technique can be used to find records with leading spaces or characters that have a special meaning for the search engine:

 

%javascript:'*';     // find trailing asterisks
*javascript:'%';     // find records with % characters

Slava

Demystifying form tabs

Posted by Slava Jul 7, 2015

There are a few mysterious things in ServiceNow that are often believed to be out of administrators’ reach. These are various pieces of the core functionality that simply work the way they do. Tabbed forms are definitely one of them. In this post, I will show you how to deal with form tabs and teach you some tricks that you are not going to find in the Wiki.

 

Toggling tabs on/off is very well explained in this post by Mark Stanger. There are, however, a few other things you may want to do with tabbed forms. The table below gives an overview of methods and properties available to you in your client-side scripts:

 

Method / PropertyDescription
g_tabs2Sections.hasTabs();Returns true if the form has any tabs and false otherwise

g_tabs2Sections.isActivated();

or g_tabs2Sections.activated
Returns true if tabbed view is ON and false otherwise
g_tabs2Sections.deactivate();Switches tabs OFF
g_tabs2Sections.activate();Switches tabs ON
g_tabs2Sections.tabs.lengthReturns the total number of tabs on the form
g_tabs2Sections.activeTabReturns the index of the currently selected tab as a String
g_tabs2Sections.setActive(0);Takes tab index* as its argument and switches to the corresponding tab
g_tabs2Sections.hideTab(2);Takes tab index* as its argument and hides the corresponding tab
g_tabs2Sections.showTab(2);Takes tab index* as its argument and reveals the corresponding tab

g_tabs2Sections.hideTabByID

(‘section_tab.65f94d7e0a0a3c0e09d606529d29a65a’);

Takes tab ID as its argument and hides the corresponding tab

g_tabs2Sections.showTabByID

(‘section_tab.65f94d7e0a0a3c0e09d606529d29a65a’);

Takes tab ID as its argument and reveals the corresponding tab

g_tabs2Sections.findTabIndexByID

(‘section_tab.65f94d7e0a0a3c0e09d606529d29a65a’);

Takes tab ID as its argument and returns the index of that tab
g_tabs2Sections.findTabIndex(‘comments’);Takes a field name as its argument and returns the index of the tab that contains the field
g_tabs2Sections.hideAll();Hides all form sections (but keeps tabs visible if in tabbed mode)
g_tabs2Sections.showAll();Unhides all form sections (except those hidden using hideTab() method) and displays them one below another
g_tabs2Sections.markAllTabsOK();Hides the red mandatory field indicator from all tabs (indicators on individual fields remain)
g_tabs2Sections.markMandatoryTabs();Displays a red indicator on tabs that have mandatory fields
g_tabs2Sections.markTabMandatoryByField(‘comments’);Takes a field name as its argument and marks the form section that contains that field mandatory (but does not prevent saving the record if all mandatory fields are in fact filled in)

 

* Tab indices start at 0

 

Examples in this table use g_tabs2Sections object and therefore work with form sections. To manage related lists in a similar manner, use g_tabs2List object instead.

 

To give you an idea of how some of these methods can be used in a real life scenario, here is a script that enables switching between form sections using Alt+PageUp and Alt+PageDown combinations of keys:

 

addLoadEvent(function() {
   $$('body').each(function (elm) {
      Event.observe(elm, 'keyup', function(event) {
         if (g_tabs2Sections.activated && event.altKey) {
            var btnCode = event.keyCode ? event.keyCode : event.charCode;
            var tabsNumber = g_tabs2Sections.tabs.length;
            var currentTab = g_tabs2Sections.activeTab * 1;
            var nextTab = 0;
            if (btnCode == 33)
               nextTab = currentTab - 1;
            if (btnCode == 34)
               nextTab = currentTab + 1;
            if (nextTab > -1 && nextTab < tabsNumber)
               g_tabs2Sections.setActive(nextTab);
         }
      });
   });
});

 

You can use it in an onLoad client script for a specific table, say Task, or in a global UI script to enable this feature for all forms in your instance.

Slava

Carbon Paper 2.0

Posted by Slava Jun 23, 2015

It is no secret for any ServiceNow administrator that there are lots of situations where you need to copy information from one record to another or even create a new record as a copy of another record. The easiest way to do it programmatically is to execute a script similar to this:

 

var usr_id = 'ddc2b4edfc81b800f0d8e21d514cc2a4';
var gr = new GlideRecord('sys_user');
gr.get(usr_id);
gr.insert();

 

The script above finds a user record by sys_id and uses insert() method to create an exact copy. Of course, two identical user records is probably not something you would really like to have in your application. In a real-life scenario, you will most likely want to change at least the name of the user. Here is a way to do it:

 

var usr_id = 'ddc2b4edfc81b800f0d8e21d514cc2a4';
var gr = new GlideRecord('sys_user');
gr.get(usr_id);
gr.first_name = 'John';
gr.last_name = 'Smith';
gr.insert();

 

There are, however, at least three things that will not be copied by a script like this.

 

1. Journal fields

 

The contents of all journal fields in the system are actually stored in Journal Entry [sys_journal_field] table. You will need to query that table to copy Work Notes or Additional Comments.

 

2. Attachments

 

When you attach a file to any record in ServiceNow, it is saved in Attachment [sys_attachment] table. To copy attachments, use the following piece of code:

 

if (typeof GlideSysAttachment != 'undefined') {
   // for Calgary and newer versions
   GlideSysAttachment.copy('source_table', 'sys_id', 'target_table', 'sys_id');
} else {
   // for Berlin and older versions
   Packages.com.glide.ui.SysAttachment.copy('source_table', 'sys_id', 'target_table', 'sys_id');
}


3. Image and video fields

 

There are two types of image fields in ServiceNow: image (e.g. Icon field in Module records) and user_image (e.g. Photo field in User records). The former contains a link to an image. The latter contains… nothing. You can easily verify that by exporting any user record that has a photo to XML. In fact, images uploaded into user_image fields are physically stored in Attachment [sys_attachment] table, the only difference from usual attachments being ZZ_YY prefix in the Table Name attribute. The same is valid for video type fields. As a result, anything you upload to Images [db_image] or Video [db_video] tables ends up in Attachments [sys_attachment] table.

Slava

Looking through your eyes

Posted by Slava Jun 16, 2015

Quite often there is a need to set up reports that return different results depending on who runs them. In versions before Dublin, it could be done fairly easily using scripted filters. With introduction of dynamic filter options in Dublin, this feature has become even more intuitive as you no longer need to memorize things like gs.getUserID() or getMyGroups(). What is not as straightforward, however, is testing of such reports.

 

In many cases, reports are created by non-admin users directly in a production instance where for security reasons you cannot allow them to impersonate other users (those who will be using the reports). Requiring all reports to be created in a test instance first is also not an option. Non-admin users will not be able to transfer reports between instances and will either have to redo them in production or involve system administrators, which is too much of an overhead. Besides, a test instance may not have the most recent data even if you clone it from production regularly.

 

If this problem sounds familiar to you, here is a workaround you might find helpful. Instead of using gs.getUserID() or getMyGroups() in reports, you can create your own functions that will return the ID or the list of groups of another user depending on the value of a user preference:

 

1. Create a very simple UI page with a reference field pointing to Users [sys_user] table. This is where your report creators will choose a user through whose eyes they want to view reports. Use processing script to save their selection as a user preference.

 

2. Make this UI page accessible via a module in the navigation pane, a UI action in the user’s profile, or elsewhere. Since the page is so simplistic, you may want to display it in a GlideDialogWindow to make it look elegant.

 

view_reports_as.png

 

3. In a global business rule, define functions that will read the user preference and return results based on its value. If no preference is set for the current user, these functions should default to the out-of-box ones, such as gs.getUserID() or getMyGroups().

 

function getRepUserID() {
    var view_as_user = gs.getUser().getPreference('view_reports_as');

    if (view_as_user == null || view_as_user == '')
        view_as_user = gs.getUserID();

    return view_as_user;
}

function getRepGroups() {
    var view_as_sys_id = getRepUserID();
    var view_as_groups = gs.getUser().getUserByID(view_as_sys_id).getMyGroups();

    return view_as_groups;
}

 

 

4. Finally, create dynamic filter options that will leverage the new functions and, optionally, hide the out-of-box ones.

 

Important! This solution does not change the security context in any way. Results returned by the reports will be constrained by security rules applicable to the current user, not the one indicated in the user preference.

Filter Blog

By date: By tag: