bookmark_borderServiceNow – Workspace – redirect

At work we have some situations, where we would like to have in some tools links pointing to certain records or a specific record within ServiceNow.

In the old platform (links like service-now.com/now/nav/ui/classic/params/target/incident_list.do) you could, at least for lists, achieve that. Perform the query (using a filter) to suit your needs and then “copy query” and adjust it to your liking in the other tool.

In the next experience workspace architecure, this isn’t quite as simple as in the old platform.

So how could I achieve this?

Initially I tried the “query” parameters route. E.g. something like sysparm_query=…

That did not work for the “/list” page. These URLs, I could not figure out differently, always use a “tiny-id” and a “list-id”. Both I cannot figure out outside ServiceNow.

Example: service-now.com/now/cwf/agent/list/params/list-id/ef9ac3b5877f121089e543f30cbb35b1/tiny-id/sIYKkcDs6HlrzFnjpxowg0iDZnaXLxgV

I then thought: maybe “/simplelist” will do! And indeed, using /simplelist you can provide query parameters, although you have to figure out the details yourself.

Example: service-now.com/now/cwf/agent/simplelist/sn_customerservice_case/params/query/stateIN10%2C18%5Eassigned_toDYNAMIC90d1921e5f510100a9ad2572f2b477fe%5Estate%3D10

It took me some trial an error to figure out how this works. Easiest way: Create the query in the old platform and replace everything after query/… with the “copied query”

But if you want to open a specific record? You’ll need, as to my current knowledge, the sys_id. In another system you have no chance knowing the ServiceNow sys_id of a record. But maybe you know a “number”, eg. an account number, a case number or something like it.

Then you can use this approach:

  1. in UI Builder create a redirect page with optional parameters. These parameters could be: table and value

In my case it was type and customerno

Now I could open any type of record(s) which can be identified by the customer number.

Add (a) client state(s) where you parse the required values from the URL you require in the datasource. If you use table field value as optional parameters, then you create for each of them an own client state.

In the page add an appropriate data source (lookup multiple records!!!!). In my example it was: getAccount, table Account, only when invoked (explicit).

In the conditions I would filter account by customerno

where the customerno was to be found in a client state field, click the db icon to be able to bind data

and bind it to the client state

You could bind the table client state to the table property of the data source. You cannot bind the field name of the condition to a client state, unfortunately. So you have to kind of hard code that in the dataSource.

The client state(s) is/are set in a Client script

function handler({api, event, helpers, imports}) {
    api.setState('customerno', api.context.props.customerno);
}

The client script is called in the page ready of the body of the page

Also I call a “refresh” data source, as the client state (customerno) now is known, the required data can be fetched.

Here I am not 100% sure. I assume the events are executed sequentially. You can change the order if you go to Settings and then select Page definition

In the Internal Event Mapping you can change the order of the events

previously

{
    "MACROPONENT_READY": [
        {
            "broker": null,
            "clientScript": null,
            "conditional": null,
            "declarativeAction": null,
            "event": null,
            "operation": {
                "dataBrokerId": "b997a84053021010cbc2ddeeff7b1228",
                "operationName": "REFRESH",
                "parentResourceId": "getAccount",
                "payload": {
                    "type": "JSON_LITERAL",
                    "value": {}
                }
            },
            "targetId": "ezQoReehhZFBkffiijjiigBSmXbeeddeeddggs",
            "type": "DATABROKER_OP"
        },
        {
            "broker": null,
            "clientScript": {
                "payload": {
                    "type": "JSON_LITERAL",
                    "value": {}
                },
                "sysId": "d03c437f872e6a109dffa7950cbb35aa"
            },
            "conditional": null,
            "declarativeAction": null,
            "event": null,
            "operation": null,
            "targetId": "UEzODvonAUxyjjiiiOGeehhdcYddIA",
            "type": "CLIENT_SCRIPT"
        }
    ]
}

after

{
    "MACROPONENT_READY": [
        {
            "broker": null,
            "clientScript": {
                "payload": {
                    "type": "JSON_LITERAL",
                    "value": {}
                },
                "sysId": "d03c437f872e6a109dffa7950cbb35aa"
            },
            "conditional": null,
            "declarativeAction": null,
            "event": null,
            "operation": null,
            "targetId": "UEzODvonAUxyjjiiiOGeehhdcYddIA",
            "type": "CLIENT_SCRIPT"
        },
        {
            "broker": null,
            "clientScript": null,
            "conditional": null,
            "declarativeAction": null,
            "event": null,
            "operation": {
                "dataBrokerId": "b997a84053021010cbc2ddeeff7b1228",
                "operationName": "REFRESH",
                "parentResourceId": "getAccount",
                "payload": {
                    "type": "JSON_LITERAL",
                    "value": {}
                }
            },
            "targetId": "ezQoReehhZFBkffiijjiigBSmXbeeddeeddggs",
            "type": "DATABROKER_OP"
        }
    ]
}

As you see I changed the order of the two events: getAccount and client script

At this point you’ll have “traced” the optional parameters in the URL with the client script and refreshed your data source to find the record(s) you need.

now create another client script (redirect) where you direct (navigate) to the page in workspace you require

{
    
    if ('services' == api.context.props.type) {
        var customerno = api.context.props.customerno;
        helpers.navigate.to('simplelist', {'table':'cmdb_ci_service'},{'query': 'company.ref_customer_account.u_customer_number=' + customerno});

    } else if ('account' == api.context.props.type) {
        helpers.navigate.to('record', {'table':'customer_account','sysId': api.data.getAccount.results[0].sys_id.value}, {'selectedTabIndex' : 1});
    } 

It took me a while to find the proper syntax to find the sys_id from the results in the data source. Also how I could select the “contacts” tab (first tab in account) to me a little while to figure out.

But when do you call the “redirect” client script?

In the Events in the data source. there’s an event “Data Fetch succeeded”

And for debugging purposes I’d recommend having a “logEvent” client script.

function handler({api, event, helpers, imports}) {
    console.log("yourPrefix->" + JSON.stringify(event, null, 4));
}

This logEvent client script you can call in any place you require more info about the result of an event. Then you can always use the Browser Debugger in the console and search for “yourPrefix->” to find interesting information. This Tip is kudos to Jesse (Debugging in UI Builder in ServiceNow) his Website

Now you can call from your other tool the URL like service-now.com/now/cwf/agent/redirect/params/type/account/customerno/20001753

This approach you could even use to create, pre-populated, records (incidents / cases)

Did you like my tutorial?

bookmark_borderUpdate selected records

Yes, there’s an OOTB functionality

Problem is: it will use the current “view” and thus, most likely, allow to change all fields.

What if you want to restrict this update to certain fields only?

First design a new “view” only containing the fields they are allowed to change on mass. It’s actually a Form Design.

Configure Form Design\New…

After creation you can switch to the newly created view (or form)

Before: Standard (or default form)

After: my stripped down form

Basic idea:

An Client side UI action where we call the dialog, call the new “submit” button, handle updates on selected records.

Code: doStuff() is set in the Onclick field of the UI action

function doStuff() {
    try {
        var d = new GlideModalForm('my dialog`s title', 'your_table_name');
        //var d = new GlideDialogForm('my dialog`s title', 'your_table_name');

	d.addParm('sysparm_view','Test_For_Blog');

	d.addParm("sysparm_view_forced", true);
	d.addParm('sysparm_form_only','true');


        // in the fixSubmit we replace the submit button with our own "submit" button
        d.setOnloadCallback(fixSubmit);
	// used for GlideDialogForm
	//d.setLoadCallback(fixSubmit);

        d.render();

    } catch (e) {
        // log your error
    }

}

// this function will be called from the newly created button
// use this function to update your selected records as you please
function mySubmit() {
    try {
        // document from dialog in iframe
        var doc = document.getElementById('dialog_frame').contentWindow.document;
        // retrieve values entered in the dialog
        var sd = doc.getElementById('sn_customerservice_case.short_description').value;

        // g_list.getChecked has a comma separated list of sys_id of records you have selected
        var recordsToUpdate = g_list.getChecked().split(',');
        recordsToUpdate.forEach(function(thisElement) {
            var gr = new GlideRecord('sn_customerservice_case');
            // retrieve record
            gr.get(thisElement);
            // update it's values, maybe add a check if there WAS a value in the dialog, otherwise you'll write '' back to the records
            gr.setValue('short_description', sd);
            gr.update();

        });
        // remove the dialog from the DOM, to prevent a nasty message telling you you might lose entered data
        document.getElementById('dialog_frame').remove();
        // refresh the list
        g_list.refresh();
        return true;

    } catch (e) {
        // log your error
    }
}

// removing the original submit button as it creates a record which we don't need
// add our own submit button which calls the mySubmit function which updates records as needed
function fixSubmit() {
    try {
        // document from dialog in iframe
        var doc = document.getElementById('dialog_frame').contentWindow.document

        // hide original submit button as it always creates a new record which we don't need
        var btnSubmit = doc.getElementById('sysverb_insert_bottom');
        btnSubmit.style.display = 'none';

        // create our own submit button
        var btn = doc.createElement("button");
        btn.innerHTML = "our own Submit button";
        btn.type = "button";
        btn.name = "formBtn";
        btn.id = "mySubmitButton";
        // the mySubmit will not be on the iframe of the dialog but on the entire page
        // that's why we need to call it via parent.
        btn.setAttribute('onClick', 'parent.mySubmit()');
        // insert the newly created button where the original has been hidden above
        btnSubmit.parentNode.insertBefore(btn, btnSubmit.nextSibling);

    } catch (e) {
         // log your error
    }

}

The crucial thing to make d.addParm(‘sysparm_view’,’Test_For_Blog’); work was the next line:

d.addParm(“sysparm_view_forced”, true);