Add a link to the Service Portal from the Next Experience UI user menu

Ryan Magsam1
Mega Guru

Has anybody tackled adding a link to the service portal in the user menu after upgrading to San Diego?

The old method on UI16 was to use an AngularJS selector and insert an html element into the menu, but that method doesn't seem to work now. I don't know enough about the new framework and React to know if there's an equivalent React method that could be used.

Screenshot of menu to avoid confusion as to which menu I'm talking about.

find_real_file.png

1 ACCEPTED SOLUTION

Ryan Magsam1
Mega Guru

Here's the solution I came up with, which is not supported in any way by ServiceNow, of course.  (They told me so in the support case I opened). This is a global UI script, just like the previous solution with UI16.

Having document objects nested in multiple shadow DOMs makes this a little more complicated than the old solution.

One improvement this needs is to stop observing the "dom:loaded" event after this is run in order to prevent this from being run more than one time. Currently it will re-run every time a glideForm or glideList is loaded. I was not able to find a solution for this, so any suggestions for improvement would be welcome.

$(document).observe('dom:loaded', function() {
    try {
        var text = getMessage('Open Service Portal') + '';

        // get document object
        // jQuery selector used to avoid a health scan flag
        // addressing the top object is necessary because the scoped window and document objects
        // in this UI script are within a shadow DOM and is not an ancestor of "macroponent"
        var d = $(top).window.document;

        // get shadow DOM of header
        var headerRoot = d.querySelector('macroponent-f51912f4c700201072b211d4d8c26010')
            .shadowRoot.querySelector('sn-polaris-layout')
            .shadowRoot.querySelector('sn-polaris-header')
            .shadowRoot;

        // prevent more than one portal link from being added
        if (headerRoot.querySelector('.custom-service-portal-link')) return;

        // get user menu component
        var menu = headerRoot.querySelector('.user-menu-controls');

        // build button element and necessary child elements for new link
        var button = d.createElement('button');
        button.className = "user-menu-button keyboard-navigatable polaris-enabled custom-service-portal-link";
        button.addEventListener('click', goToPortal);

        var div = d.createElement('div');
        div.className = "user-menu-label polaris-enabled";

        var icon = d.createElement('now-icon');
        icon.className = "user-menu-icon polaris-enabled";
        icon.setAttribute("icon", "square-share-fill");

        button.append(div);
        div.append(icon);
        div.append(text);

        // add button element to user menu
        menu.append(button);

    } catch (e) {}

    function goToPortal() {
        $(top).window.location.href = '/sp';
    }
});

View solution in original post

5 REPLIES 5

Mohith Devatte
Tera Sage
Tera Sage

Hello @Ryan Magsam did you try the method by creating a UI script and adding  through angular element ?is it that you are referring to is not working ?

Yes, for UI16 we currently use the following script in a global UI script.  I miss-spoke thinking it was an angular selector, but looks like it was actually a jQuery selector. I've attempted replicating this method by modifying the CSS selector and HTML element to match the new DOM, but so far have been unsuccessful.

I'm going to keep working on this and will post the solution here, unless someone finds one before me. I'm hoping to avoid directly addressing the document object since that will flag us on ServiceNow's health scan.

$(document).observe('dom:loaded', function() {
	try {
		var text = getMessage('Open Service Portal') + '';
		var menu = $j('#user_info_dropdown', parent.document).next();
		if (menu) {
			var html = menu.html();
			var newOpt = '<li><a href="./sp">'+ text +'</a></li>';
			if (html.indexOf('/wm_sp') < 0) {
				menu.prepend(newOpt);
			}
		}
	} catch (e) {
	}
});

Ryan Magsam1
Mega Guru

Here's the solution I came up with, which is not supported in any way by ServiceNow, of course.  (They told me so in the support case I opened). This is a global UI script, just like the previous solution with UI16.

Having document objects nested in multiple shadow DOMs makes this a little more complicated than the old solution.

The only improvement this needs is to stop observing the "dom:loaded" event after this is run in order to prevent this from being run more than one time. Currently it will re-run every time a glideForm or glideList is loaded. I was not able to find a solution for this, so any suggestions for improvement would be welcome.

$(document).observe('dom:loaded', function() {
    try {
        var text = getMessage('Open Service Portal') + '';

        // get document object
        // jQuery selector used to avoid a health scan flag
        // addressing the top object is necessary because the scoped document object
        // in this UI script is run within a shadow DOM and is not an ancestor of "macroponent"
        var d = $(top).window.document;

        // get shadow DOM of header
        var headerRoot = d.querySelector('macroponent-f51912f4c700201072b211d4d8c26010')
            .shadowRoot.querySelector('sn-polaris-layout')
            .shadowRoot.querySelector('sn-polaris-header')
            .shadowRoot;

        // prevent more than one portal link from being added
        if (headerRoot.querySelector('.custom-service-portal-link')) return;

        // get user menu component
        var menu = headerRoot.querySelector('.user-menu-controls');

        // build button element and necessary child elements for new link
        var button = d.createElement('button');
        button.className = "user-menu-button keyboard-navigatable polaris-enabled custom-service-portal-link";
        button.formAction = "/sp";
        button.type = "submit";

        var div = d.createElement('div');
        div.className = "user-menu-label polaris-enabled";

        var icon = d.createElement('now-icon');
        icon.className = "user-menu-icon polaris-enabled";
        icon.setAttribute("icon", "square-share-fill");

        button.append(div);
        div.append(icon);
        div.append(text);

        // add button element to user menu
        menu.append(button);

    } catch (e) {}
});

Ryan Magsam1
Mega Guru

Here's the solution I came up with, which is not supported in any way by ServiceNow, of course.  (They told me so in the support case I opened). This is a global UI script, just like the previous solution with UI16.

Having document objects nested in multiple shadow DOMs makes this a little more complicated than the old solution.

One improvement this needs is to stop observing the "dom:loaded" event after this is run in order to prevent this from being run more than one time. Currently it will re-run every time a glideForm or glideList is loaded. I was not able to find a solution for this, so any suggestions for improvement would be welcome.

$(document).observe('dom:loaded', function() {
    try {
        var text = getMessage('Open Service Portal') + '';

        // get document object
        // jQuery selector used to avoid a health scan flag
        // addressing the top object is necessary because the scoped window and document objects
        // in this UI script are within a shadow DOM and is not an ancestor of "macroponent"
        var d = $(top).window.document;

        // get shadow DOM of header
        var headerRoot = d.querySelector('macroponent-f51912f4c700201072b211d4d8c26010')
            .shadowRoot.querySelector('sn-polaris-layout')
            .shadowRoot.querySelector('sn-polaris-header')
            .shadowRoot;

        // prevent more than one portal link from being added
        if (headerRoot.querySelector('.custom-service-portal-link')) return;

        // get user menu component
        var menu = headerRoot.querySelector('.user-menu-controls');

        // build button element and necessary child elements for new link
        var button = d.createElement('button');
        button.className = "user-menu-button keyboard-navigatable polaris-enabled custom-service-portal-link";
        button.addEventListener('click', goToPortal);

        var div = d.createElement('div');
        div.className = "user-menu-label polaris-enabled";

        var icon = d.createElement('now-icon');
        icon.className = "user-menu-icon polaris-enabled";
        icon.setAttribute("icon", "square-share-fill");

        button.append(div);
        div.append(icon);
        div.append(text);

        // add button element to user menu
        menu.append(button);

    } catch (e) {}

    function goToPortal() {
        $(top).window.location.href = '/sp';
    }
});