Open Site Navigation

Workspace UI Actions

Updated: Oct 21



I find myself starting posts on creating components, then begin going down the wormhole of what can be done in Workspace. I'm still exploring showing components in modal windows, and need to finish my article on that and the Modal component, but in doing that I came across the g_modal API and I think it's fitting to document it first, before suggesting going down those routes.


g_modal


I wish this was documented more. I think the non-documentation of this API is going to cause developers confusion and push them to un-necessary component building in my opinion. Since there is no official documentation, this is just stuff I've come across scouring the community and decided to document here. To my knowledge g_modal can do four things:


  1. Display input type fields within a modal window

  2. Show something else in a frame, such as a UI Page or external link

  3. Do something on confirm

  4. Load a Component

There may be more, and if you know of them, please comment below! This is all done in the Workspace Client Script section of a UI Action (and make sure you have your UI Action sent to Client)


Common Properties


There are some common properties that will apply to all three of these options.


title: The title of the Modal window
size: Size of the Modal window: sm, lg, xl, fw (fixed-width)
height: Height in pixels/em if you choose, not mandatory}

showFields


This method will display a modal window with the fields defined in the UI Action Workspace Client Script. It displays the OK and Cancel buttons by default.


Properties:


fields:
name: Whatever you want to name this field, or if you're using a reference field, the name of the reference field on the form
type: textarea, choice, reference, boolean, string
label: How the field displays to the user
value: Initial value of the field, used more in choices or perhaps booleans
mandatory: true or false

If type is reference:


name: The reference field on the current record being used to search
reference: The table you're referencing
referringTable: The table you're referencing from
referringRecordId: The sys_id of the value you're calling the UI Action from
query: Can use a simple query here (maybe even a script include, haven't tried) such as 'active=true'

I did want to point out the then() method which returns a Promise. This returns whatever the modal returns, for example fieldValues in this example, but also whatever a component returns (read more below) and then you can do something with the data returned.


Here's a simple example of using one field:


function onClick(g_form) {
    g_modal.showFields({
        title: "Enter your reason",
        fields: [{
            type: 'textarea',
            name: 'work_notes',
            label: getMessage('Reason'),
            mandatory: true
        }],
        size: 'lg'
    }).then(function(fieldValues) {
        g_form.setValue('work_notes', fieldValues.updatedFields[0].value);
        g_form.save();
    });
}


In this example I set the work notes field on the client side with the field values that are returned are in the updatedFields array. Since I only had one field, its place was 0, if you have multiple fields, you'll just enter the position they're in. Try a console.log to see whats in there.


Here's another example with a few more fields:


function onClick(g_form) {

    var fields = [{
        type: 'textarea',
        name: 'work_notes',
        label: getMessage('Reason'),
        mandatory: true
    },
        {
            type: 'choice',
            name: 'reason_code',
            label: getMessage('Reason code'),
            value: getMessage(' -- Select -- '),
            choices: [{
                displayValue: 'Because I want to',
                value: 'illdowhatiwant'
            },
                {
                    displayValue: 'Not sure',
                    value: 'notsure'
                }
            ],
            mandatory: true
        },
        {
            type: 'reference',
            name: 'caller_id',
            label: getMessage('What is your name?'),
            mandatory: true,
            reference: 'sys_user',
            referringTable: 'incident',
            referringRecordId: g_form.getUniqueValue()
        }
    ];

    g_modal.showFields({
        title: "Enter your reason",
        fields: fields,
        size: 'lg'
    }).then(function(fieldValues) {
        g_form.setValue('work_notes', fieldValues.updatedFields[0].value);
        g_form.setValue('caller_id', fieldValues.updatedFields[2].value);
        g_form.save();
    });
}



A good out-of-the-box example is the Copy Incident UI Action available in your PDI. In this example, a GlideAjax call is being used to a Script Include to create the Incident record, and then the GlideAgentWorskpace (g_aw) method is being used to redirect to that method afterwards.


I'd say this is a good place to start if you're looking to replace some less complex UI Pages that take some input from the user and create/update a record and re-direct, rather than looking at custom components right away.


/nav_to.do?uri=sys_ui_action.do?sys_id=7678d7d2531232000600e26b88dc3401


function onClick(g_form) {
    getMessages(["Copy Incident", "Caller", "Failed to copy Incident."], onCopy);

    function onCopy(tMsgs) {
        var fields = [];
        fields.push({
            type: 'reference',
            name: 'caller_id',
            label: tMsgs["Caller"],
            mandatory: true,
            reference: 'sys_user',
            referringTable: 'incident',
            referringRecordId: g_form.getUniqueValue()
        });

        g_modal.showFields({
            title: tMsgs["Copy Incident"],
            fields: fields
        }).then(function(fieldValues) {
            var updatedFields = {};
            fieldValues.updatedFields.forEach(function(field) {
                updatedFields[field.name] = field.value;
            });
            var ga = new GlideAjax('IncidentUtils2');
            ga.addParam('sysparm_name', 'makeIncidentCopy');
            ga.addParam('sysparm_sys_id', g_form.getUniqueValue());
            ga.addParam('sysparm_fields', JSON.stringify(updatedFields));
            ga.getXMLAnswer(function(response) {
                if (response + '' !== "false")
                    g_aw.openRecord("incident", response + "");
                else
                    g_form.addErrorMessage(tMsgs["Failed to copy Incident."]);
            });
        });
    }
}

showFrame


You can link to outside URLs, or even UI Pages. Here's a simple example of linking to a knowledge article, I'm sure this could be used for a variety of things, such as linking to portals, third-party sites, etc.


function onClick(g_form) {
    var kbId = '24d9243187032100deddb882a2e3ec33';
    g_modal.showFrame({
        url: '/kb_view.do?sys_kb_id=' + kbId,
        title: 'Test Knowledge Article',
        size: 'lg',
        height: 500
    });
}


UI Pages are a little more tricky especially with the cancel, or X functionality, as the Processing script seems to run even on cancel. I know I've had to deal with some set in stone or more complex UI Pages already that I just couldn't port over into a component right away, so a good example of how SN is doing this is the Consume UI Action:


/nav_to.do?uri=sys_ui_action.do?sys_id=9c9563073723100044e0bfc8bcbe5dcb

function onClick(g_form) {
    var stock = g_form.getDisplayValue('stockroom');
    var assetName = g_form.getValue('display_name');
    var assetSysId = g_form.getUniqueValue();
    g_modal.showFrame({title: assetName, url: '/consume_a_consumable.do?sysparm_sys_id='+assetSysId+'&sysparm_stockroom='+stock+'&from_workspace=true', size:'lg'});
}

Here's a screenshot of what that looks like (I increased the height), if you want to check it out, just plugin in the alm_consumable table into your workspace URL, and find a sys_id of a consumable that is 'In Stock':



Some things I want to call out in the UI Page that I noticed ServiceNow doing as a workaround:


The following fields were added in the HTML:


<input type="hidden" id="cancelled" name="cancelled" value="false"/>
<input type="hidden" id="from_workspace" name="from_workspace" value="false"/>

In the CSS, they're only allowing GlideDialogWindow.get().destroy() if the function isActionfromWorkspace is false:


function cancel() {
    var c = gel('cancelled');
    c.value = "true";

    // Only destroy the window when user is on platform view
    if (!isActionFromWorkspace())
        GlideDialogWindow.get().destroy();
}

/**
/**
 * Determine if the action performed was from platform or on workspace
 */
function isActionFromWorkspace() {
    var fromWorkspace = getParmVal('from_workspace') === "true";

    // Set the hidden input field that determined whether or not the
    // submission was from platform or workspace
    var c = gel('from_workspace');

    if (fromWorkspace)
        c.value = "true";
    else
        c.value = "false";

    return fromWorkspace;
}

/**
 * Helper function to get the parameter values from the URL
 *
 * @param {string} name of the url parameter to fetch
 */
function getParmVal(name) {
    var url = document.URL.parseQuery();
    if (url[name]) {
        return decodeURI(url[name]);
    }
    return;
}

All of this stuff in the Client script seemed like overkill in my opinion, in my UI Action, I just sent a parameter to fill in the from_workspace field value, basically if I was calling it from the Platform UI script, I'd set the value I was sending to 'false' and if I was calling it from the Workspace Client Script, I'd set the value I was sending to 'true'.


Finally, in the Processing script, a check is done to see if cancelled is not true, assuming that OK or any other button was clicked, and send a re-direct if we're not calling this from workspace.


if (cancelled == "false" && (asset != "" || user != "")) {
    var new_sys_id = new Consumables().splitForeground(consumable, qty, '10', '', asset, '', '', user);
}

if (from_workspace !== "true")
    response.sendRedirect("alm_consumable.do?sys_id=" + consumable);

So the challenge is, we can't use the same redirect method in workspace, it will just re-direct in the modal window you're and then probably disappear quickly. There's probably more eloquent solutions to this (comments please!) but as a quick workaround, what I did was call a script include in a callback to check to see if a record matching certain conditions was created in the past minute. If not, then assume cancel was clicked, if so, then assume the processing script ran and did its thing and re-direct to the record or show a message. I'm in no way advocating this is best practice, just a means to an end, or perhaps a jumping off point for better solutions.


function onClick(g_form) {

    var taskRecord = g_form.getUniqueValue();
    var anotherRecord = g_form.getUniqueValue();
    var workspaceUrl = 'true';

    var url = '/your_ui_page.do?sysparm_task_record=' + taskRecord + '&sysparm_another_record=' + anotherRecord + '&sysparm_from_workspace=' + workspaceUrl + '&sysparm_stack=no&sysparm_workspace=' + true;
    g_modal.showFrame({
        title: "Testing UI Page",
        url: url,
        size: 'md',
        height: '18rem',
        callback: function() {
            var ga = new GlideAjax('MyClientScriptInclude');
            ga.addParam('sysparm_name', 'theFunctionToCall');
            ga.addParam('sysparm_task', g_form.getUniqueValue());
            /* In this example I created a new record type in the processing script and it will link to the current record I 'm on in the workspace, so I need to send the current record to the script include. */
            ga.getXMLAnswer(function(answer) {
                if (!answer || answer == '') {
                    return;
                } else {
                    g_form.addInfoMessage('Something has been created'); //Or you can open the record in a new tab with:
                    g_aw.openRecord("table", 'sys_id_from_answer_or_other_source');
                }
            });
        }
    });
}

Update (2/26/2021):


In the comments Ronald Löb informed me that you could access data from the UI Page using window.parent.postMessage() in the UI page to pass data from the iFrame back into the parent workspace window, since g_form cannot be accessed from the UI Page due to being in a iFrame. This is going to solve a lot of transition from Platform to Workspace problems in my opinion with some minor UI Page refactoring.


He mentioned the Propose Major Incident UI Action as a good example of this. It's interesting how ServiceNow codes two different ways in their own platform sometimes with the UI Action/Page I discussed above, versus the Propose Major Incident functionality. In my opinion I'd say look at this one for examples as it's more well written, and handles the cancel button issue I spoke about previously.


I won't post the whole code here but will post some links and point out some things:


If you look at lines 35-84 in the HTML of the Propose Major Incident UI Page, you'll see the iFrame Message Handler code being used to communicate back with the UI Action/Workspace:

/nav_to.do?uri=sys_ui_page.do?sys_id=3c02c808873013000e3dd61e36cb0b71

Here's the solution ServiceNow is using to handle clicking cancel:

function close() {
    if(!config.workspace)
        dialog.destroy();
    else
        window.location.href = window.location.href + '&sysparm_next_pg=true';
}

Finally, here's the UI Action callback:

callback: function (ret, data) {
    if (ret)
        proposeMIC(data);
}

I did log 'data' and saw the following, my text was 'work note note' and 'business impact note'

data: {"msg":"Proposed as major incident candidate","workNotes":"work note note","businessImpact":"business impact note"}

This is the more eloquent solution of doing this, and I'm definitely looking forward to implementing this solution more when I refactor UI Pages for the workspace, thank you Ronald!



Confirm


Properties:


confirm: getMessage(Title of the Modal), msg(Text to the user), function (confirmed){
    if (confirmed){
//do something
    }
}

/nav_to.do?uri=sys_ui_action.do?sys_id=9dd8a44867103300a0bd35e643415a8b

function onClick(g_form) {

    if (g_user.userID != g_form.getValue('assigned_to')) {
        g_modal.alert('Only the assignee of this interaction can end the chat.');
        return;
    }

    var msg = getMessage("Are you sure you want to end this chat?");
    g_modal.confirm(getMessage("Confirmation"), msg, function (confirmed) {
        if (confirmed) {
            g_form.setValue('state', 'closed_complete');
            g_form.save();
        }
    });

    return false;
}

This method is tucked away in the End Chat UI Action on Interactions and is a good example of using if statements to determine when a user can take an action and performing an action when the user hits OK versus Cancel.



Component


Properties:


confirmTitle: Name of your confirm button
cancelTitle: Name of your cancel button
params: Passed to the inner component, this is where you pass sysIds, or other values that your component would need

I'm not going to go into depth on creating a component here and linking it up, as I have another article in the wings about that. I will say, you'll need to register your component in the Registered Scripting Modals table. Dylan Lindgren created a great article about creating a component and embedding it into a UI action that explains the process.


The Create Change Request example out-of-the-box is a good one to look at to see how the code is formatted. It pops up an interceptor (which is in the registered modals table) and opens a record based on which choice was selected in the component, for example if the change is Standard, it uses a GlideAjax call to work it's magic and open a new tab to the Standard Change Catalog, and if it's not Standard, then opens a new record in a tab. You can see in the code, after the g_modal the scope of the registered modal is sn_itsm_workspace and the name is showInterceptor. Basically you'll copy over whatever is in the Public API field of the Registered Scripting Modal record.


/nav_to.do?uri=sys_ui_action.do?sys_id=1d850156230123001488dc1756bf6504

function onClick(g_form) {
    getMessages(['Create Change Request', 'Create', 'Cancel'], openInterceptorModal);

    function openInterceptorModal(msg) {
        var result = g_form.submit('create_std_change');
        if (!result) {
            return;
        }
        result.then(function() {
            g_modal.sn_itsm_workspace.showInterceptor({
                title: msg['Create Change Request'],
                confirmTitle: msg['Create'],
                cancelTitle: msg['Cancel'],
                size:'sm',
                height:'md',
                params: {"modal": "false","target":"change_request"}})
                .then(function(modalResult){
                    if (modalResult.data) {
                        if (modalResult.data.table != 'sc_cat_item')
                            g_aw.openRecord(modalResult.data.table, modalResult.data.sys_id, {query:modalResult.data.query});
                        else {
                            var ga = new GlideAjax('StdChangeUtils');
                            ga.addParam('sysparm_name', 'ajaxFunction_getCategory');
                            ga.getXMLAnswer(function (answer) {
                                if (answer) {
                                    var params =modalResult.data.params;
                                    params.sysparm_parent_table = "incident";
                                    params.sysparm_parent_sys_id = g_form.getUniqueValue();

                                    g_service_catalog.openCatalogItem('sc_cat_item', '-1', params);
                                }
                            });
                        }
                    }
                });
        });
    }
}


Summary


I think I captured mostly everything with this method, but probably not. I think it's going to be super handy when working with Agent Workspace or even other workspaces in dealing with UI Actions, because most organizations are going to have something they put in and want to convert over. I hope I shed some light on some of the other ways this API can be used outside of components in order to reduce component sprawl for simple things. I encourage you to explore the out-of-the-box UI Actions that are using this API in your PDI to get ideas. I want to thank the pioneers in the community who posted questions on this undocumented API and Dylan Lindgren for his great article on how to use it with components.


Resources


This is a great community article on showFields:


I'll plug Dylan's article again because it's so helpful.


ServiceNow Product Documentation: GlideAgentWorkspaceAPI


6,311 views2 comments