- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
01-04-2023 03:41 PM - edited 01-04-2023 03:51 PM
I have an onSubmit client script with which I'm trying to pull the list of usernames from a cost center entered by the user, and populate those names in to a list collector.
The script kind of works. However in the ITIL view I'm only getting 1 entry in the list collector when I know there are 12 or so employees under the selected cost center. From the Portal i'm not getting any entries at all.
Script is as follows
function onSubmit() {
var costCenter = g_form.getValue('u_current_cost_center');
// Get the list of users with the specified cost center
var users = new GlideRecord('sys_user');
users.addQuery('cost_center', costCenter);
users.query();
// Add the employees to the u_employee_names list collector field
while (users.next()) {
g_form.setValue('u_employee_names',users.sys_id);
}
}
Solved! Go to Solution.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
01-10-2023 12:38 PM - edited 01-15-2023 11:23 PM
In short GlideAjax is a class written by ServiceNow, that leverages the standard XMLHttpRequest - the only way to communicate with a web server after a page is loaded. To be more precise, when I say the only way, I'm talking about XMLHttpRequest.
Anyway, GlideAjax is a client side component that enables talking to the server after page load. It needs a - so called - "handler" on server side - a Script Include, that needs two characteristics:
- to extend class AbstractAjaxProcessor and
- to be marked as "Client callable":
Hint: when creating such Script Includes, always mark checkbox "Client callable" right after entering the Script Include name, before doing any modifications to the Script Include boiler plate code - only in this case the system turns the regular Script Include boiler plate into a GlideAjax suitable Script Include boiler plate.
Next one adds methods that will be "called" by the GlideAjax component; e.g:
var UserAjax = Class.create();
UserAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
'getUsersByCostCenter': function () {
return JSON.stringify([gs.getUserID()]);
},
'type': 'UserAjax',
});
These handler methods should return strings - the only data type that can be transmitted "through the wire" (from server to client). If complex data needs to be sent through, the easiest is to "generate" an object than JSON.stringify() that object, so it becomes a string.
Of course in the final implementation one would load all users belonging to a const center and return an array with their sys_ids - here I am returning an array with a single item, the current user.
On client side, in the onChange event handler one would call this Script Include and its method as below:
function onChange (control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue === '')
return;
// Indicate which Script Include contains the handler method
var gx = new GlideAjax('global.UserAjax');
// Indicate with method should be called
gx.addParam('sysparm_name', 'getUsersByCostCenter');
// Add a value to the payload - in this case the id of the Cost Center
// selected - it is assumed that this onChange event handler is for the
// variable in which the Cost Center is selected
gx.addParam('sysparm_value', newValue);
// Initiate the Ajax call indicating which function to call when the data
// from server is received by the browser
gx.getXMLAnswer(receiveUsersByCostCenter);
}
function receiveUsersByCostCenter (payload) {
// Turn the string sent by the server back into an object/array
var users = JSON.parse(payload);
g_form.setValue('<glide list variable name>', users.join(','));
// Probably:
//g_form.setValue('u_employee_names', users.join(','));
}
Well, I suppose that's actually usable already, only thing needed is to plug in the correct variable name where the setValue method is called.
Server sides in the Script Include created, in the method called, one can use this.getValue() to get to the data sent over from the client side (set by instruction gx.addParam('sysparm_value', newValue);):
var UserAjax = Class.create();
UserAjax.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
'getUsersByCostCenter': function () {
var gr = new GlideRecord('sys_user'),
users = [];
// Validate and set the query in one go
if (gr.isEncodedQueryValid('cost_center=' + this.getValue())) {
gr._query();
// Add selected users to the array which will be sent to the
// client side
while (gr._next())
users.push(gr.getUniqueValue());
}
// Return the list of users retrieved or an empty list if the user
// lookup did not succeed for some reason
return JSON.stringify(users);
},
'type': 'UserAjax',
});
Well, actually this should already solve the requirement. But note that this is not secure at all: anyone can "manually" call the same method, so you should add protection to method getUsersByCostCenter to only return data if the user qualifies, e.g. has a specific role. Unless you don't care whether any authenticated user can or cannot get the list of users belonging to any of the Cost Centers.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
01-24-2023 02:55 PM
Here's my answer to the deleted question :-):
The function that runs server side and answers the call from client side builds just a list of user sys_ids (getUniqueValue() returns the sys_id of the current record). So what you send and is set as value of the list collector looks something like:
83708f5a47282110d2689995536d43cb,de808b1e47a42110d2689995536d43fc,d4908b1e47a42110d2689995536d43e4,...
What happens is that after you set the value of the list collector to such a list, the system initiates a separate, follow-up Ajax call, sends back your list and fetches the names (display values) for those users.
So my comment
// Return the list of users retrieved or an empty list if the user
is actually (sorry for it) misleading in that it is a list of user sys_ids, not users.
In case of reference or choice fields/variables, where you also have a value and a display value, you have the option to provide both in one go. In those cases indeed it is worth fetching not just the value/sys_id, but the display values too. Function g_form.setValue() accepts 3 parameters, the 3rd one being used in such cases. So instead of just providing the sys_id, e.g:
g_form.setValue('<reference variable/field name>', 'd4908b1e47a42110d2689995536d43e4');
one can write:
g_form.setValue('<reference variable/field name>', 'd4908b1e47a42110d2689995536d43e4', 'John Doe');
and thus the system will not do a follow-up call to fetch the display value (John Doe).
This can be used for glide lists too, but it is quite complicated to do so and it does not work in classic CMS or UI16, just Portal.
Extra care must be exercised when building the list of names, as those must not contain commas. If the list you provide as 3rd parameter to setValue() contains more commas then the list you provide as 2nd parameter to setValue(), it will be ignored and the system falls back to doing the extra follow up call to fetch the display values for all items in the list.
If we apply the strategy of 3rd parameter to setValue() the example code might look something like:
- Script Include:
var UserAjax = Class.create();
UserAjax.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
'getUsersByCostCenter': function () {
var gr = new GlideRecord('sys_user'),
userUniqueValues = [],
userDisplayValues = [];
// Validate and set the query in one go
if (gr.isEncodedQueryValid('cost_center=' + this.getValue())) {
gr._query();
// Add selected users to the array which will be sent to the
// client side
while (gr._next()) {
var displayValue = gr.getDisplayValue();
var normalizedDisplayValue = displayValue.replaceAll(',', '');
userUniqueValues.push(gr.getUniqueValue());
userDisplayValues.push(normalizedDisplayValue);
}
}
// Return the list of users retrieved or an empty list if the user
// lookup did not succeed for some reason
return JSON.stringify({
uniqueValues: userUniqueValues,
displayValues: userDisplayValues
});
},
'type': 'UserAjax',
});
- Catalog Client Script:
function onChange (control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue === '')
return;
// Indicate which Script Include contains the handler method
var gx = new GlideAjax('global.UserAjax');
// Indicate with method should be called
gx.addParam('sysparm_name', 'getUsersByCostCenter');
// Add a value to the payload - in this case the id of the Cost Center
// selected - it is assumed that this onChange event handler is for the
// variable in which the Cost Center is selected
gx.addParam('sysparm_value', newValue);
// Initiate the Ajax call indicating which function to call when the data
// from server is received by the browser
gx.getXMLAnswer(receiveUsersByCostCenter);
}
function receiveUsersByCostCenter (payload) {
// Turn the string sent by the server back into an object/array
var users = JSON.parse(payload);
g_form.setValue('<glide list variable name>', users.uniqueValues.join(','), users.displayValues.join(','));
// Probably:
//g_form.setValue('u_employee_names', users.uniqueValues.join(','), users.displayValues.join(','));
}
In this setup still just two attributes of each user is transmitted (the unique value - sys_id and the display value).
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
01-24-2023 03:10 PM
I was looking to re word my last question because I think I answered it AS i was asking. I discovered that I could also push other fields this way, in the 'getUsersByCostCenter'
while (gr._next())
users.push({ sys_id: gr.getUniqueValue(), manager: gr.getValue('manager') });
}
I haven't yet decided if this is useful yet, but is interesting to know. The code changes you provided above are extra useful
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
01-24-2023 03:12 PM
Yes, you are right, you can push as complex information as you want, if you pack it into an object. In fact it is a good strategy to fetch as much as possible in as few calls as possible.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
01-24-2023 03:15 PM
Lets say I push the getUniqueValue and the manager, those would go into 2 separate arrays correct? One containing the list of sys_id's and the other containing the manager?
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
01-24-2023 03:19 PM - edited 01-24-2023 03:21 PM
As far as transport is concerned, it doesn't matter. I chose to do my example as you just described it, because the response is easier to consume on client side. So having two separate arrays allows me to just take the arrays and slap those in as parameters. If I were to do it so that I have an array of objects that each contain multiple properties of users, then I would need to first extract a list of sys_ids and a list of names and only then could I use the so created lists as parameters.