FootPrints Service Core Community Blog

Creating searches that automatically adapt to the user who runs them

Posted by on Jun 9, 2015 in FPSC | 0 comments

Goal

Create saved searches that give the correct results no matter which agent runs them such as “Submitted by Me” and “My Overdue Assignments“. This is a sample application of the JavaScript injection technique taught in this previous post.

Applicability

FootPrints Service Core version 11 (any sub-release) running on Microsoft Windows Server 2003/2008/2012.

Prerequisites

  • Access to IIS on FootPrints application server
  • Access to the FootPrints file folder on the application server
  • URL Rewrite extension for IIS

Background

FootPrints Service Core version 11 has a limited number of built-in searches that dynamically apply the current user’s identity when they run. For example, all agents have access to the “My Assignments” saved search that gives each agent their respective assignments even though there is only one copy of this search. Searches like this behave as though there were an inherent “me” parameter in the assignment condition that gets filled in at runtime. There is no way to create your own searches with this capability. If you want all agents to have access to a search that shows their overdue assignments or any other custom-defined criteria, you have to create a separate search for each and every agent! As another example, you can’t create and roll out a “Submitted by Me” search to all agents; you actually have to create a “Submitted by Bob” search, a “Submitted by Jane” search, and so forth.

I was happy to see that version 12 provides a @Me placeholder you can use in the “assigned to” and “created by” conditions of a custom saved search. (For you version 12 stakeholders out there, a similar idea has been proposed on the BMC Community forum to support a @MyTeams placeholder.)  Version 11, however, requires a custom solution which I will put forth here.  

The Solution

First, you will create an advanced search and save it as “Shared (Internal)” so that other agents will be able to access it. You can define any complex criteria you need but remember to leave either the Assignee or Submitter field alone depending on which one you want to have populated at runtime when an agent runs the search.

So now you have your search, but because it has no constraint on the assignee or submitter field, it is not very useful. In order to have this constraint added at runtime, you’ll need to inject JavaScript onto the homepage for all agents. The technique I use for this is to use an URL Rewrite rule in IIS to load a custom JavaScript source file called GlobalJavascript.js. This source file will be loaded on every page in the FootPrints interface and you can put any code in it that you want. Details for this technique are offered here: Tutorial: Customizing FootPrints Service Core using JavaScript, Part 2. Assuming you aren’t already using that technique to push custom JavaScript out to the general FootPrints interface, you can create the GlobalJavaScript.js file using the code below. Otherwise, just take the parameterizedSearches function and the line that calls it from the code below and merge them into your existing file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
{
// DO NOT CHANGE THIS BLOCK
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = '/tmp/javascript/ResizeDocumenT.js';
    head.appendChild(script);
    function AddWindowResizeEvent() {
        setTimeout(function() { 
            AddWindowResizeEvent(arguments);
        }, 100);        
    }
// END "DO NOT CHANGE THIS BLOCK"
}
 
parameterizedSearches();
 
function parameterizedSearches() {
    // only run this customization on the homepage.
    if (document.location.pathname.search('/MRcgi/MRhomepage.pl') == -1) {
        return;
    }
    // INSTRUCTIONS:
    // Populate the "searchesToParameterize" list with the searches that should take a parameter 
    // at runtime for either the assignee or submitter condition. 
    // The ids for searches can be found in \etc\SearchList. 
    // The id for cross-workspace searches can be found in \etc\crossProjectSearches\SearchList*.txt 
    var searchesToParameterize = [ 
        { 
            searchId: '1:PROJECT:SEARCH:Open__bassigned__bto__bme', 
            parameter: 'assignee' 
        },
        { 
            searchId: '1:PROJECT:SEARCH:Created__bby__bMe__blast__bmonth', 
            parameter: 'submitter' 
        }
    ]; 
 
    var mapParam = { 
        submitter: 'ORIGINATOR', 
        assignee: 'ASSIGNEDTO' 
    }; 
 
    if (document.dropDown != undefined && document.dropDown.SAVEDNAME != undefined) { 
        originalProcessDisplayDropdown = processDisplayDropdown; 
 
        processDisplayDropdown = function(refresh, useMaxMinInc, isMobile) { 
 
            var form = document.dropDown; 
 
            var selectedSearch = form.SAVEDNAME.value; 
 
            // See if the selected search matches any that should be parametrized 
            var parametrizedSearch = null; 
            for (var i = 0; i < searchesToParameterize.length; i++) { 
                var id = searchesToParameterize[i].searchId; 
                // cross-workspace searches do not begin with number 
                if (id.match(/^\d/) == null) { 
                    id = "CROSSPROJ:" + id; 
                } 
                if (selectedSearch.indexOf(id) == 0) { 
                    parametrizedSearch = searchesToParameterize[i]; 
                    break 
                } 
            } 
 
            if (parametrizedSearch !== null) { 
                var input = document.createElement('input'); 
                input.type = 'hidden'; 
                input.name = mapParam[parametrizedSearch.parameter]; 
                input.value = form.USER.value; 
                form.appendChild(input); 
            } 
 
            originalProcessDisplayDropdown(refresh, useMaxMinInc, isMobile); 
        }; 
    } 
}

At line 28, I have configured this customization to run with 2 saved searches. You need to replace this code so that it lists your own search(es). Each search is listed as an object (within { } braces) having a searchId property and a parameter property. To find the searchId property for your saved search, you’ll have to open the \etc\SearchList file under the main FootPrints folder on your server. This file is a bit difficult to read, so here are some tips: Each line of the file represents a saved search or report. Each line contains 4 tokens (pieces of data) separated by colons. The first token is the ID of the workspace to which the search/report belongs. The second token is the owner — the term “PROJECT” indicates that the search/report is shared for agents to access.  The next token should read “SEARCH” for searches, “FORMAT” for reports, or “TIME” for time tracking reports, and the final token is the search’s name with spaces and other characters encoded as ‘__b’, etc.

The parameter property should be either 'assignee' or 'submitter' depending on which field you want to provide dynamically using the id of the agent running the search.

You can add as many of these objects as you want, but remember to separate them with commas. Here’s what that area of code would look like if you only wanted to include 1 search:

var searchesToParameterize = [ 
    { 
        searchId: '1:PROJECT:SEARCH:Open__bassigned__bto__bme', 
        parameter: 'assignee' 
    }
];

and here’s what it would look like if you had 3 searches:

var searchesToParameterize = [ 
    { 
        searchId: '1:PROJECT:SEARCH:Open__bassigned__bto__bme', 
        parameter: 'assignee' 
    },
    { 
        searchId: '1:PROJECT:SEARCH:Created__bby__bMe__blast__bmonth', 
        parameter: 'submitter' 
    },
    { 
        searchId: '1:PROJECT:SEARCH:My__bOverdue__bTickets', 
        parameter: 'assignee' 
    }
];

Caveats

Once you’ve created a saved search and set up the JavaScript file and the URL Rewrite rule that sends the JavaScript to the agents’ web browsers,  you can tell your agents to include the search(es) on their homepages by changing their Preferences. To run the search, the agents would simply select the search from the list on their homepage as they normally would:

saved search There are a few caveats to be aware of, however:

  • This customization is desiged to work for agents only, not customers.
  • The runtime substitution of the agent’s ID into the search only takes place when the search is run by selecting it from the list on the homepage. If an agent has the search set as their default search, the custom behavior won’t take place when the agent first logs in or clicks the Home button.
  • The custom behavior will not occur if one of the searches is launched from the My Searches page (although the user can get the correct behavior by clicking the refresh icon after the search comes back with its results.)
  • With the code provided above, only the assignee and submitter fields can be supplied at runtime. Adding support for any arbitrary field is possible but beyond the scope of this sample.

Need help?

For help writing a FootPrints Service Core customization, or to discuss a project that might require a custom solution, contact RightStar Technical Services.

Disclaimer

In no event shall RightStar Systems, Inc. or its employees be held responsible for loss or damages arising out of the use, inability to use, or the results of use of, the information and source code contained in this article. The information and materials contained or referred to in this article are for reference only. RightStar Systems, Inc. and its employees make no warranty of any kind regarding the information and source code in this article. In no event shall RightStar Systems, Inc. or its employees be obligated to provide additional support on the topic discussed in this article.

 

Tutorial: Customizing FootPrints Service Core using JavaScript, Part 2

Posted by on Aug 27, 2014 in FPSC | 0 comments

Tutorial: Customizing FootPrints Service Core using JavaScript, Part 2

Goal

Learn how to inject custom JavaScript onto any page in FootPrints.

Applicability

FootPrints Service Core version 11 (any sub-release) running on Microsoft Windows Server 2003/2008/2012.

Prerequisites

  • Access to file system on FootPrints application server
  • Access to IIS on FootPrints application server
  • URL Rewrite extension for IIS
  • Ability to write JavaScript
  • (Suggested) Familiarity with the Sencha Ext JS framework (version 3.4, specifically)
  • (Suggested) Familiarity with the HTML Document Object Model (DOM)

It’s well known that the incident, problem, change request, and other forms can be customized in FootPrints Service Core with injected JavaScript. Just look on the BMC Communities forums, or see my previous post in this series, which fully documents the process*. What’s new and exciting for me right now is a technique I recently discovered that allows you to customize ANY page in FootPrints:

  • Want to add instructions to the login form so that users will know they should be logging in with their Active Directory credentials?
  • Want to add a button to the chat feature that creates a new issue with a transcript of the conversation?
  • Want to make saved searches available to customers?
  • Want to have a single saved search that checks for issues assigned to or submitted by the currently logged-in agent, determined at runtime?

All of these things are possible, and they’re really just the tip of the iceberg. This technique opens the door for virtually any custom behavior; your willingness and ability to write the JavaScript for your desired behavior is really the only possible limiting factor.

Back Story

Earlier this year, a client asked if it would be possible to create a saved search that any agent could use to find their assignments in a particular category. In other words, they wanted a single, shared search that would apply a condition at runtime based on whichever agent was running the search. FootPrints comes with a small number of searches that behave this way, like “My Assignments” and “My Active Approvals”, but  it doesn’t let administrators create their own. The workaround is to tell each and every agent to create their own version of the search that has their own user id hardcoded into one of the conditions. That’s hardly practical. (Thankfully, version 12 introduces this capability.)

Changing the source code was not an option for this project, so I started thinking about using JavaScript. The challenge was finding a reliable way to get this JavaScript onto the agents’ homepage. Each user could run a browser extension such as TamperMonkey that injects JavaScript on pages matching certain URL patterns. I knew there was no way they would go for that. Or, I could deploy a dashboard widget to every agent’s homepage that contained the JavaScript. Hmmm…but what happens if the agent deletes that widget? I happened to know that there was one JavaScript file that gets included on every page. I could modify it, I thought, but then there would be the chance that the client wouldn’t know to back it up before running an upgrade. The solution I came up with was to hijack all requests for that JavaScript file, sending back a file of my own instead. The key to making this happen was IIS’s URL Rewrite module.

The Solution

Just about every webpage in FootPrints includes a file called ResizeDocument.js. I created a URL Rewrite rule that looks for requests for this file and redirects them to my own script file. My own script file would carry out whatever customizations I wanted to implement, and, acting responsibly, tell the page to make another request for the ResizeDocument.js it originally requested. In order to prevent the second request for ResizeDocument.js from also being redirected, I wrote the second request with a slight change in capitizalization such that it does not match my URL Rewrite rule.

Let’s set up that new JavaScript file first. Call this file GlobalJavaScript.js and place it in the C:\FootPrintsServiceCore\html directory (adjust drive letter and/or FootPrints root directory as needed.) Below is how this file should be defined initially:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
window.addEventListener("load", function() {
    // DEFINE YOUR CUSTOM BEHAVIOR HERE
 
    // END CUSTOM BEHAVIOR
});
 
{
// DO NOT CHANGE THE REMAINDER OF THIS FILE
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = '/tmp/javascript/ResizeDocumenT.js';
    head.appendChild(script);
    function AddWindowResizeEvent() {
        setTimeout(function() { 
            AddWindowResizeEvent(arguments)
        }, 100);
    }
}

Notice the last couple of lines are telling the page to request a JavaScript file that has a random capital ‘T’ in its name. That capital ‘T’ is what allows this second request for that file to bypass the URL Rewrite rule, which we’re going to setup as case-sensitive.

Next, open IIS and open the FootPrints web site, which is listed as “Default Web Site” in most cases. Click on the virtual directory named “tmp” and you should see the URL Rewrite module. As a side note, if you don’t have this module installed, it means you can’t possibly be running FootPrints in FastCGI mode. I strongly recommend you put reading this tutorial on hold, open up the FootPrints documentation and search for “FastCGI”.

nav to correct virtual directory

IIS setup: Opening the URL Rewrite feature for the correct virtual directory

Double-click the URL Rewrite module. Click “Add Rule(s)…” from the menu of Actions on the right. On the next window, double-click the first option to create a blank, inbound rule:

blank rule

IIS setup: Creating a blank inbound rule.

The next window has a lot of options.

  • The first two dropdowns should read “Matches the Pattern” and “Regular Expressions”.
  • For Pattern, type in ^ResizeDocument\.js
  • Make sure Ignore case is UNchecked.
  • Jump down to Action type and select “Rewrite”
  • For Rewrite URL, type in /footprints/GlobalJavaScript.js
  • Check the option to Append query string
  • Log rewritten URL is probably better left unchecked, but it won’t have any effect on how the rule works either way. It only affects the IIS  request logs.

The complete rule configuration should look like this:

new rule

IIS setup: settings for the new inbound rule

After saving the rule, if you go to the C:\FootPrintsServiceCore\html\tmp\javascript directory, the web.config file should look like this:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="Global JavaScript Injection">
                    <match url="^ResizeDocument\.js" ignoreCase="false" />
                    <action type="Rewrite" url="/footprints/GlobalJavaScript.js" logRewrittenUrl="true" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>

Of course, you could also just create a web.config file containing the above text and drop it into the C:\FootPrintsServiceCore\html\tmp\javascript\ directory rather than setting up the rule through IIS.

Getting back to the GlobalJavaScript.js file we created, you’ll see there is a space at the top where you are supposed to put your custom code. Do not feel like you are rushed to edit the file with your code. As it stands, the file will cause no custom behavior or disruption. I recommend that you take your time developing the code, pasting it from a text editor into your browser’s developer tools, or using a browser extension that runs custom JavaScript such as GreaseMonkey or TamperMonkey.

The window.addEventListener("load"...) construct is intended to delay your code from running until the web page has fully loaded. If your code requires more accurate timing than this, there are ways to do that, but I won’t get into them here.

Here are some tips to help you get started writing your code:

  • Have your code check the current URL to determine what page is being shown. For example,
    if (window.location.pathname.search("/MRcgi/MRhomepage.pl") > -1)

    tells you that the homepage is being shown.

  • Certain Perl scripts have more than one function. For example, MRsearch_page.pl is used for creating reports, advanced searches, and escalations. You will need to look at more than just window.location.pathname to determine which specific case is running. You could look at window.location.search, which shows all of the parameters in the URL after the “?”. For example, the create/edit escalation page can be detected with this condition:
    if (window.location.search.search("ESCALATIONPAGE=1") > -1)
  • Not all FootPrints URLs have a set of parameters after a “?” that JavaScript can inspect. In these cases, window.location.search will be an empty string. If you need to determine what page is running, you’ll have to look for some HTML element or combination of elements that identifies the page. For instance,
    if (window.location.search == "" 
        && document.faqs !=== undefined && document.faqs.nodeName == "FORM" 
        && document.faqs.SOLSEARCH !== undefined)

    will tell you that the customer’s simple knowledge base search page is being shown. It’s checking that there’s a <form> element named “faqs” on the page that has an input named “SOLSEARCH“. I arrived at this condition by going to that page and using my browser’s developer tools to inspect certain HTML elements that I thought would be indicative of the scenario I wanted my code to identify.

  • If you want to use JavaScript to make an HTTP request to another FootPrints page, let’s say in a popup window, at a minimum, the request must pass the logged-in user’s id, session key, and current workspace id. These parameters are USER, MRP, and PROJECTID, respectively. Requests made for customer-level users will also need to have a CUSTM parameter. If these parameters are not present in the current page’s URL, you can collect them from one of the <form> elements (the navigation toolbar contains several) on the page:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    // Gather USER, MRP, PROJECTID, and possibly CUSTM, parameters from page.
    // Check all forms on document until one can supply values for all needed parameters.
    var capturedParams = { };
     
    for (var i = 0; i < document.forms.length; i++) {
        var form = document.forms[i];
     
        var neededParams = ['USER', 'MRP', 'PROJECTID'];
        var paramsFoundThisForm = 0;
     
        // look for each needed parameter:
        for (var j = 0; j < neededParams.length; j++) {
            var p = neededParams[j];
            if (form.elements[p] !== undefined && form.elements[p].value != "") {
                capturedParams[p] = form.elements[p].value;
                paramsFoundThisForm++;
     
                // MRP beginning with a 0 signals this is a customer user.
                // The CUSTM parameter will also need to be passed in the request.
                if (p == 'MRP' && form.elements[p].value.match(/^0/)) {
                    neededParams.push('CUSTM');
                }
            }                
        }
     
        // Check if this form supplied all needed parameters.
        if (paramsFoundThisForm == neededParams.length) {
             // Launch a new ticket window with parameters in URL
             var baseUrl = "/MRcgi/MRTicketPage.pl";
     
             var keyValuePairs = [];
             capturedParams.MAJOR_MODE = "CREATE";
             for (var p in capturedParams) {
                 if (capturedParams.hasOwnProperty(p)) {
                     keyValuePairs.push(p + "=" + encodeURIComponent(capturedParams[p]));
                 }
             }
     
             window.open(baseUrl + "?" + keyValuePairs.join("&"));
     
             break;
         }
    }

 

Caveats

  1. IIS cannot be running in a case-sensitive mode where it considers xyz.html to be a different resource than XYZ.html. IIS is case-insensitive by default, and I couldn’t even tell you how to change that setting, but I figured I should stipulate that anyway.
  2. You might run across a page that does not include ResizeDocument.js. Our technique of rewriting requests for that particular file won’t work on those pages. See if there’s another JavaScript file being loaded that you can construct another rewrite rule for to handle such cases.
  3. I haven’t personally run into this problem, but in theory, browser cache might interfere with the rewrite mechanism after you first create the rewrite rule. You (and other FootPrints users) might need to clear your browser’s cache or force a no-cache refresh of FootPrints by holding the  CTRL key and clicking the refresh button.

Coming Soon

My next post will describe a complete use case for this JavaScript injection technique. Most likely, I will explain how to create shared searches that change according to which user runs them, as I mentioned in the back story for this post.

Need help?

For help writing a FootPrints Service Core customization, or to discuss a project that might require a custom solution, contact RightStar Technical Services.

 *The previous post in this tutorial series is not required reading to follow this tutorial, but it does contain some best practice suggestions for writing JavaScript to extend FootPrints in general.

Disclaimer

In no event shall RightStar Systems, Inc. or its employees be held responsible for loss or damages arising out of the use, inability to use, or the results of use of, the information and source code contained in this article. The information and materials contained or referred to in this article are for reference only. RightStar Systems, Inc. and its employees make no warranty of any kind regarding the information and source code in this article. In no event shall RightStar Systems, Inc. or its employees be obligated to provide additional support on the topic discussed in this article.

Tutorial: Customizing FootPrints Service Core using JavaScript, Part 1

Posted by on Jul 11, 2014 in FPSC | 0 comments

Tutorial: Customizing FootPrints Service Core using JavaScript, Part 1

Goal

Learn best practices for customizing the FootPrints ticket page with JavaScript.

Applicability

FootPrints Service Core version 11 (any sub-release)

Prerequisites

  • Access to Form Designer in one or more workspaces
  • Access to file system on FootPrints application server
  • Ability to write JavaScript
  • (Suggested) Familiarity with the Sencha Ext JS framework (version 3.4, specifically)
  • (Suggested) Familiarity with the HTML Document Object Model (DOM)

One of the things I love most about FootPrints is how readily customizable it is. I’m not talking about the fact that administrators are able to tweak so many options through the web interface, or that the source code is uncompiled plain text that can be edited by anyone who knows how to program Perl, but the fact that the makers of FootPrints have always kept open the means to inject JavaScript onto the incident form. It’s been very rare that a client will ask if something is possible in FootPrints and my answer is not “Yes, with some JavaScript.”

In the first part of this tutorial, we will see how to customize the incident form using JavaScript added through the Form Designer. The business case we will use as an example is one where the organization wants a particular field to appear above the Assignees picker in the Assignees and Notifications section of the incident form. The Form Designer does not allow fields to be placed in this section, establishing the case for a JavaScript customization. While this particular use case may not be of any interest to your organization, by following this tutorial, you will receive advice on best practices and find code that can be reused in your own customizations.

Let’s say your organization wants to assign incidents automatically to different groups based on categorization fields. Your categorization is based on two dropdown fields, Category and Sub Category, and so the standard Auto Assignment feature, which can only be based on a single field, cannot be used to determine assignees. Instead, the more flexible AutoField feature must be used. The potential problem with AutoField is that it runs each and every time an incident is updated and we don’t want the assignees to change if they’ve been set by other means throughout the incident’s lifecycle. We’re going to let the editing agent decide on a case-by-case basis whether the assignees should be subjected to the AutoField rules by introducing a field called Let Categories Determine Assignees. Each AutoField rule will be set up to run only if this field is set to “Yes”, like so:

AutoField rules for setting assignees

AutoField rules for setting assignees. Each rule mandates that “Let Categories Determine Assignees” be set to “Yes”.

Our JavaScript is going to accomplish the following things:

Rename the first option in the Let Categories Determine Assignees field from the built-in value of “No Selection” to “No”.
This makes the field a little more user-friendly. If the user wants to override the auto-assignment behavior, they won’t be scratching their heads wondering what the effect of “No Selection” might be.

Default Let Categories Determine Assignees to “No”.
Prevents auto-assignment from occurring each and every time the incident is edited.

Set Let Categories Determine Assignees to “Yes” when categorization fields change.
Necessary to have the auto-assignment rules run after the incident is created or updated.

Move the Let Categories Determine Assignees field to the assignees area.
Intended to improve usability. If the user wants to manually set assignees, they’ll open the assignees section of the form where they’ll be more likely to notice this field and understand that they have to change it from “Yes” to “No”.

The first step in this tutorial is to create a new text file called incident_management.js inside the C:\FootPrintsServiceCore\html\ directory (throughout this tutorial, we’ll be assuming FootPrints is installed to C:\FootPrintsServiceCore.) Paste the following code into the file and save the file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// Moves the "Let Categories Determine Assignees" field 
// to the assignees and notifications section.
// Defaults this field to "No.
// Sets this field to "Yes" when the category fields are selected
function assigneeCustomizations() {
 
    var categoryFields = ["Category", "Sub Category"];
 
    // Only run this customization under certain conditions
    if (isDetailsMode || isCustomerMode) {
        return;
    }
 
    // field to be moved
    var fName = "Let Categories Determine Assignees";    
    var f = document.getElementById(fName.fix());
    // verify that the field to be moved exist on the form as a dropdown
    if (f === null || f.type != 'select-one') {
        return;
    }
 
    // move the field and label above the assignee picker.
    // verify that the assignees picker is displayed.
    if (window.assigneePicker !== undefined) {
        var originalDivHoldingFieldAndLabel = Ext.get(f).up('div.cell'); 
 
        var tbody = Ext.get(assigneePicker.box1).up('tbody');
 
        var td = new Ext.Element(document.createElement('td'));
        td.set({
            colSpan: tbody.child('tr').query('td').length
        });
 
        td.appendChild(originalDivHoldingFieldAndLabel);
        var tr = new Ext.Element(document.createElement('tr'));
        tr.appendChild(td);
 
        tbody.insertFirst(tr);
    }
 
    // Set listeners on the category fields so that when they change,
    // the field to set assignees based on categorization 
    // will switch to 'Yes'
    for (var i = 0; i &lt; categoryFields.length; i++) {  
        var c = document.getElementById(categoryFields[i].fix());
        if (c !== null) {
            Ext.EventManager.on(
                c,
                'change',
                function() {
                    f.selectedIndex = 1;
                }
            );
        }
    }
 
    // Rename "No Selection" to "No"
    f.options[0].text = 'No';
 
    // Always default or reset the field to not override assignees
    // based on categorization
    f.selectedIndex = 0;
}
 
Ext.onReady(function() {
    afterFormInitialized([setGlobals, assigneeCustomizations]);
});
 
// Supporting code follows. This can be included in
// other custom javascript files with no change.
var isDetailsMode;
var isEditMode;
var isCreateMode;
var isAgentMode;
var isCustomerMode;
 
function setGlobals() { 
    if (document.regform === undefined) {
        isDetailsMode = true;
    }
    else {
        if (document.regform.MAJOR_MODE !== undefined) {
            var mode = document.regform.MAJOR_MODE.value;
            if (mode == "CREATE" || mode == "PREVIEW") {
                isCreateMode = true;
            }
            else if (mode == "EDIT") {
                isEditMode = true;
            }
        }
    }
 
    // determine if customer or agent user.
    // MRP parameter begins with 0 if customer.
    for (var i = 0; i &lt; document.forms.length; i++) {
        var f = document.forms[i];
        if (f.elements.MRP !== undefined) {
            var mrp = f.elements.MRP.value;
            if (mrp.charAt(0) == '0') {
                isCustomerMode = true;
            }
            else {
                isAgentMode = true;
            }
            break;
        }
    }
}
 
function afterFormInitialized(callbacks) {
    Ext.TaskMgr.start({
        run:  function() {
            var el = Ext.get('wait_c') || Ext.get('waitP_c');
            if (el === null) {
                // the "Loading..." mask hasn't been created yet
            }
            else {
                if (!el.isVisible(true)) {
                    Ext.each(callbacks, function(i) { i.call()});
                    return false;
                }
            }
        },
        interval: 50
    });   
}

 

Next, we’ll use the Form Designer to add a Custom HTML element to the form that references the file we just created.

Adding a Custom HTML element to the incident form

The “Custom HTML” form element is listed under the “Special Features” section of the Form Designer palette.

Form Designer palette with Custom HTML highlighted

Form Designer palette with Custom HTML highlighted

Because of how the code was written [I’ll come back to this], we are free to place the Custom HTML element anywhere on the form. I prefer to drop it into the area above the tabbed sections so that it will always be prominent when viewing the Form Designer. You should be aware that while the Custom HTML element we are going to add is not going to contain any visible content, it will still occupy a space on the form’s layout grid. If you put it on a row by itself, users will see a blank row on the form. For that reason, I recommend you put it in an unoccupied space on a partially filled row.

Placement of Custom HTML element above tabbed sections

Placement of Custom HTML element above tabbed sections

Once the element has been placed on the form, click the pencil icon to edit its properties.

Populating the Custom HTML element's body

Populating the Custom HTML element’s body

In this example, set the element’s name to “Incident Management Customizations”.

Next, disable Rich Text Mode.

Important: you must disable Rich Text Mode on a Custom HTML element that holds raw HTML or JavaScript

and enter the following content:

<script src="/footprints/incident_management.js"></script>

Set the element’s Permissions and Access properties so that it is visible to both customers and agents. You should always do this, even if you have a customization that should not run, let’s say, on the customer version of the incident form.

Next, add a dropdown field to the form called Allow Categories to Determine Assignees. This field should have only 1 choice, “Yes”, and should be optional to agents/hidden to customers.

That’s it for the step-by-step portion of this tutorial. What remains is to create and populate the Category and Sub Category fields, and to create some AutoField rules that set the assignees field based on combinations of these two fields plus the Allow Categories to Determine Assignees (having a value of “Yes” in each rule). I assume the reader knows how to perform these basic administrative tasks already. I will now move on to highlighting a number of techniques used in this example that I consider to be best practices.

Best Practices

B.P. #1: Delay your code from running until the form has finished initializing

If you have ever experimented with adding JavaScript using the Form Designer before, you have probably had to ask yourself the question “Where is the best place [on the form] to add my code?” Placed too near the top and it might try to address a field before it has been rendered. Adding a fixed amount of delay before the code runs might work most of the time but inexplicably fail other times. The reason it is a hard question to answer is that browsers use multiple, parallel threads to render the page, fetch resources over the network, and execute JavaScript, and these threads do not execute in any predictable order.

The technique I advocate does not make a loose prediction and leave things to chance. It delays the code from running for as long as needed until it receives a signal that indicates with 100% certainty that the form has finished loading. The code below demonstrates how to call the myCustomCode1 and myCustomCode2 functions after the form has finished initializing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Your customizations go inside functions whose names are
// passed into afterFormInitialized.
function myCustomCode1() {
     alert("The form is ready.");
}
function myCustomCode2() {
     alert("I also think the form is ready.");
}
Ext.onReady(function() {
    afterFormInitialized([myCustomCode1, myCustomCode2]);
});
 
function afterFormInitialized(callbacks) {
    Ext.TaskMgr.start({
        run:  function() {
            var el = Ext.get('wait_c') || Ext.get('waitP_c');
            if (el === null) {
                // the "Loading..." mask hasn't been created yet
            }
            else {
                if (!el.isVisible(true)) {
                    Ext.each(callbacks, function(i) { i.call()});
                    return false;
                }
            }
        },
        interval: 50
    });   
}

You can see this technique in practice in our example code: the two functions, setGlobals and assigneeCustomizations, are passed into afterFormInitialized, delaying their execution until the appropriate time.

B.P. #2: Place your JavaScript in a Custom HTML form element

Back in the days before FootPrints Service Core had its Form Designer feature, custom JavaScript had to be injected onto the ticket form using what were called “field headers”. Field headers were always tied to a specific field on the form and they were always printed (even if they contained only invisible JavaScript) above the field they were tied to. The equivalent of a field header in version 11 is the “Help Text and Instructions” property of a field. There are a couple of reasons I do not recommend using a field’s “Help Text and Instructions” property to hold your JavaScript. The first is that it makes the JavaScript hard to locate: you cannot see at a glance whether a field’s Help Text and Instructions holds JavaScript, actual instructions for the user, or both. You might be thinking that keeping track of where JavaScript is located can be made into a straightforward task by always putting the JavaScript in the instructions property of the particular field it affects, but what you have to remember is that not all customizations target a specific field. The other reason I do not use any field’s instructions property is that it forces you to use extra care when changing a field’s visibility to particular user roles or when deleting a field, as the injection of the JavaScript will be affected by such changes.

With the introduction of the Form Designer in version 11, you can now add Custom HTML elements to the form. By using one of these to hold your JavaScript, you will avoid the two problems mentioned earlier: you can give the HTML element a name that makes its purpose (holding JavaScript) self-evident, allowing you to quickly locate where you JavaScript resides without drilling down into multiple fields; custom HTML elements are not tied to any fields, allowing you to manage fields and custom JavaScript independently.

B.P. #3: Load an external script file

When we created the Custom HTML element, we did not fill it with any actual JavaScript; instead it only contains a <script> tag that loads an external file containing the JavaScript.   The main reason I advocate this practice is that it avoids an issue where + characters can disappear from JavaScript defined directly in the Form Designer. This can be a serious problem given the role + plays in JavaScript syntax:

Imagine your code includes the for loop:

1
2
3
for (var i = 0; i &lt; 5; i++) {
	x = x * x;
}

One day, you make a change to some unrelated part of the form and publish. Suddenly, no one can create any incidents–the browser just hangs. It turns out that the + characters in the JavaScript disappeared, leaving your code looking like this:

1
2
3
for (var i = 0; i &lt; 5; i) {
	x = x * x;
}

Your incident form now has an infinite loop because the loop’s counter variable, i, never changes!

Keeping the JavaScript in an external file also facilitates easier maintenance of the code. You can write your code in your favorite text editor instead of having to use the Form Designer interface. You can also keep as many different versions and backups of the code as you need just by copying the file to different names.

B.P. #4: Keep all of your JavaScript for a given workspace together

You might be wondering why I chose to name the Custom HTML element “Incident Management Customizations”, as that name doesn’t give any indication as to what the code actually does. The file name referenced by this Custom HTML is given a similarly generic name, “incident_management.js”. This is intentional. I recommend defining all customizations for a given workspace in a single file for the following reasons:

  • Fewer files that have to be transferred to the browser
  • More control over the order in which the customizations run
  • Less chance of naming collisions resulting from a global function or variable having been defined more than once across multiple files

There are a couple of caveats to be aware of, however. First, if you are editing the JavaScript file on a live system, any syntax error you might introduce can break all of the other JavaScript customizations defined in that file. Therefore, you really must test your changes on a development server, or by making your changes in a copy of the JavaScript file, editing your Custom HTML element to load that new file, and then validating the code using the Form Designer’s Preview mode. Once the code has been tested, you can discard the draft of the form and replace the .js file currently referenced by the Custom HTML element. Second, because you will not be defining your customizations in the Help Text and Instructions property of the specific fields they interact with, you will not be able to control the inclusion of the JavaScript based on whether the field it is associated with appears on the incident form using the options shown here

Options for conditionally displaying a field's Help Text and Instructions

Options for conditionally displaying a field’s Help Text and Instructions

That’s okay, though, because you can instead take a JavaScript-only approach to conditionally having portions of your code run or not, and this will actually give you more control than you could get by using the Form Designer’s options for conditionally displaying the Help Text and Instructions content.

B.P. #5: Take a JavaScript-only approach to running your code under certain conditions

So far, I’ve advocated using a single Custom HTML element to inject all of your JavaScript onto the form, and allowing the entirety of this JavaScript to appear on the form unconditionally, without any regard for whether certain fields referenced by the JavaScript are actually present for the given user on the given incident at the given time. You might then be wondering how we are going to have the code run only under certain targeted scenarios such as whether the form being displayed is for creating or editing an incident, whether that form is being accessed by a customer- or agent-level user, whether that user has rights to see a certain field, or even whether the time is currently after midnight on the 4th Friday of the month. The answer is to have the code itself check that all the proper conditions for running a certain portion of custom code are met.

You can see this technique in our example code where the function setGlobals defines a number of flags such as isCustomerMode and isDetailsMode that are later checked throughout the code. For instance, our example customization only runs on the create and edit forms opened by an agent:

9
10
11
12
    // Only run this customization under certain conditions
    if (isDetailsMode || isCustomerMode) {
        return;
    }

If another customization were later added to the JavaScript file, that customization could also make use of these flag variables. The setGlobals function is suitable for reuse in your own customizations.

The Form Designer lets you show or hide the Custom HTML element that loads your JavaScript based on user role. If the element is to be shown, then all of the JavaScript will be loaded; it’s an all-or-nothing deal. This JavaScript-only technique, by contrast, produces JavaScript that assumes it will always be present on the incident form and takes responsibility for controlling which parts execute. This allows for much greater flexibility than can be achieved through the Form Designer options, as you can have single lines of code execute or not based on the current user type, or any other condition you care to check.

B.P. #6: Check form elements (fields) before attempting to access them

Given that the JavaScript is going to be present on all forms, as just mentioned, it follows that your code should always check for the existence of form elements before attempting to reference them. In our example, no assumption was made that the assignees field is displayed. Perhaps the user accessing the form is an agent whose role does not allow specifying assignees. Therefore, the entire block of code that tries to reference the assignees picker is surrounded by the following conditional statement:

if (window.assigneePicker !== undefined) { 
    [do stuff to assigneesPicker]
}

Similarly, the code makes no assumption that any of the categorization fields are present:

var c = document.getElementById(categoryFields[i].fix());
if (c !== null) { 
    [do stuff to c]
}

Whenever I’m not sure whether my code should be comparing against null or undefined,  I open up my browser’s developer tools and test some on-the-fly JavaScript:

6-undefined-vs-null

B.P. #7: Reference fields by their internal names (without needing to know how they’re encoded)

In FootPrints, a field’s internal name is formed by encoding all non-alphanumeric characters in the visible name into a 3-character sequence. Sub Category has the internal name of Sub__bCategory, Phone # is Phone__3, and so on.  If you need to access the form input for a field (to get or set its value), you will need to know this internal name because that’s how the form inputs are named. However, you don’t have to know the actual internal name and all of its special character encodings. Your code can make use of the fix and unfix functions that are available on all String objects. This is what we’re doing in our example:

    var fName = "Let Categories Determine Assignees";    
    var f = document.getElementById(fName.fix());

It reads a lot better than

    var fName = "Let__bCategories__bDetermine__bAssignees";

B.P. #8: Don’t change field names

Changing a field’s name through the Form Designer will change the name of the corresponding column in the database and the name of the HTML input on the incident form. This could adversely affect reporting tools that read the database, clients of FootPrints’ web services, and JavaScript customizations that interact with the form inputs. If you do need to change a field’s name, consider using the “Language of Workspace and Address Book Fields” feature to do so, rather than changing the name through the Form Designer. This feature merely changes the displayed name of the field in the graphical interface.

Changing a field's label but not its internal name

Changing a field’s label but not its internal name

Keep in mind that the field’s internal name and therefore the name of the form input for the field will always be the encoded version of its original name; i.e., the name of the field before it has been re-labeled for a particular language.

Until next time

If you’ve taken away anything from this tutorial, I hope it’s what I put forward as a re-usable basis for any incident form customization you might ever need to write:

  1. Create a single JavaScript file for the workspace
  2. Add the afterFormInitialized function to the file
  3. Add the setGlobals function to the file
  4. Call afterFormInitialized as follows
    Ext.onReady(function() {
        afterFormInitialized([setGlobals]);
    });
  5. Add a Custom HTML element to your form to load the JavaScript file

Once you have that in place, just add a function to the file that performs the real work of your customization, and pass this new function into the afterFormInitialized call

Ext.onReady(function() {
    afterFormInitialized([setGlobals, myCustomization]);
});

The next tutorial will cover a method for injecting JavaScript on any page in FootPrints, not just the incident form. I discovered this method during the last year and I am really excited to reveal it. Until then, I hope this tutorial has prepared you to start writing your own JavaScript customizations for FootPrints.

Need help?

For help writing a FootPrints Service Core customization, or to discuss a project that might require a custom solution, contact RightStar Technical Services.

 

Disclaimer

In no event shall RightStar Systems, Inc. or its employees be held responsible for loss or damages arising out of the use, inability to use, or the results of use of, the information and source code contained in this article. The information and materials contained or referred to in this article are for reference only. RightStar Systems, Inc. and its employees make no warranty of any kind regarding the information and source code in this article. In no event shall RightStar Systems, Inc. or its employees be obligated to provide additional support on the topic discussed in this article.

Show Buttons
Share On Facebook
Share On Twitter
Share On Google Plus
Share On Linkdin
Hide Buttons