UI Script - redefine angular directive in ServiceNow

Andrii
Kilo Guru

Hi,

very interesting task I've got - I need to modify/replace part or whole angular directive that is used in ServiceNow, but not available for editing.

So I know the name of app, the name of directive - I have the code (read-only) ... but I don't know how to replace code of that directive with my own code - because I need to change behavior according to requirements.

Any good ideas superSnowMen?) May be with UI Script somehow?

1 ACCEPTED SOLUTION

Andrii,



I got my hands on a Helsinki dev instance. I installed the plugins that bring in the Demand Workbench. And I apologize but I didn't know that the Demand Workbench really isn't a Service Portal page. I assumed that it was because of the angular script you presented. So, I started digging more into to see what I can learn about it.



It looks to me that this is a UI Page that is only accessible on the ServiceNow side of things where only ServiceNow employees can touch the actual code. Or it could be generated from a Processor. Knowing the ServiceNow Platform you can usually override parts such as UI pages, UI macros and UI scripts by creating the same type within your instance with the same name. Then when the system runs it will use your custom page/macro/script instead of the OOB.



On the instance I was using I couldn't find the UI Script holding the angular script (sn.bubbleChartVisualization). So, I couldn't create a UI Script with the same name because I didn't know the actual name of the UI Script. I ended up taking a different route. I created a UI page with the script taken from the $demand_workbench page (recovered via Inspect Element > Sources click on $demand_workbench.do?.... ). I named the UI page demand_workbench.



I gave it a try and looked like everything worked as normal as I tried all the features comparing to the OOB $demand_workbench page. Then I created a UI script with the script you posted to disable the bubble context menu. Brought that UI script into the UI page (<script></script>) and that seemed to work; meaning the bubble context menu was disabled.



Below is the screenshot of the custom demand workbench page:


demand_workbench.png



Keep in mind I didn't do a thorough test of it. But I thought it might give you some ideas of what can be done. That whole process really took about 10min since all of the code was already done.


I hope this helps in some way.



Here's a screencast overview of what I did. (Excuse the recording. It's not the best)


Custom Workbench UI Page


View solution in original post

12 REPLIES 12

Ok, so what I understood so far is that your answer would work only if I want to have directive override for directive within a widget. But I need to override the directive within OOTB Demand Workbench.



So I found these simple examples of directive override in angular:




and I want to do something similar within ServiceNow.



With that in mind, I've created UI Script with following code:



(function() {


angular.module('sn.bubbleChartVisualization').config(function($provide) {



$provide.decorator('bubbleContextMenuDirective', function( $delegate ) {



var directive = $delegate[0]; // Get needed Directive from Array of directives


var link = directive.link; // Save Link Fn into new variable



directive.compile = function(element, attrs) {



return function(scope, element, attrs) {



var viewLabel = i18n.getMessage("View");


var createLabel = i18n.getMessage("Create");


var viewDemandLabel = i18n.getMessage("View Demand");



scope.menuWidth = 0;


scope.menuHeight = 20;


scope.cx = 0;


scope.cy = 0;


scope.point = {};


scope.showContextMenu = false;



scope.menuOptions = [{


name: viewLabel,


action: 'relatedEntityAction',


y: scope.menuHeight * 0


},


{


name: viewDemandLabel,


action: 'openDemand',


y: scope.menuHeight * 1


}];




/* OVERRIDING / DISABLING */



scope.$on('BUBBLE_CONTEXT_MENU_CLICKED', function (evt, point) {


chartCtrl.showMenu(false);


});






scope.doAction = function (name) {


if (name) {


scope[name]();


}


};


scope.openDemand = function () {


if (scope.point.sys_id) {


openWindow(DEMAND_FORM_URL + scope.point.sys_id + NOSTACK_URL_SNIPPET);


}


};


function openWindow(url) {


$window.open(url);


}


function getTextWidth() {


var elem = element.find('text');


return findMaxWidth(elem);


}


function findMaxWidth(elem) {


var largest = 0;


for (i = 0; i < elem.length; i++) {


if (coordinateSystem.elementWidth(elem[i], 135) > largest)


largest = coordinateSystem.elementWidth(elem[i], 135);


}


return largest;


}


function setupCoordinates() {


var menuXNeed = chartCtrl.cx + chartCtrl.bubbleRadius + scope.menuWidth;


var menuYNeed = chartCtrl.cy + chartCtrl.bubbleRadius + scope.menuOptions.length * scope.menuHeight;


if (menuXNeed > coordinateSystem.plotWidth) {


scope.cx = chartCtrl.cx - chartCtrl.bubbleRadius - scope.menuWidth - 10;


} else {


scope.cx = chartCtrl.cx + chartCtrl.bubbleRadius - 10;


}


if (menuYNeed > coordinateSystem.plotHeight) {


scope.cy = chartCtrl.cy - chartCtrl.bubbleRadius - scope.menuOptions.length * scope.menuHeight;


} else {


scope.cy = chartCtrl.cy + chartCtrl.bubbleRadius;


}


}


function createRelatedEntity(point) {


demandPoints.createRelatedEntity(point.sys_id, point.type).then(function (relatedEntityObj) {


var relatedEntityFormUrl = getRelatedEntityFormUrl(relatedEntityObj.sys_id, point.type);


var relatedEntityNumber = relatedEntityObj.number;


var relatedEntityTypeLabel = relatedEntityObj.label;


var link = '<a id="entity_creation_link" target="_blank" href="' + relatedEntityFormUrl + '">' + relatedEntityNumber + "</a> ";


var msg = i18n.getMessage("Demand create message");


var notification;


msg = msg.replace("{0}", relatedEntityTypeLabel);


msg = msg.replace("{1}", link);


notification = {


type: 'info',


message: msg


};


$rootScope.$broadcast('showNotification', notification);


$rootScope.$broadcast('refreshList');


});


}



function openRelatedEntity(point) {


openWindow(getRelatedEntityFormUrl(point[point.type], point.type));


}



function getRelatedEntityFormUrl(sysId, type) {


return demandPoints.relatedEntitiesConfig[type]['form'] + '?sys_id=' + sysId + NOSTACK_URL_SNIPPET;


}



scope.relatedEntityAction = function () {


if (demandHasRelatedEntity())


openRelatedEntity(scope.point);


else


createRelatedEntity(scope.point, scope.point.type);


scope.showContextMenu = false;


};



// OVERRIDE


/*


scope.$watch(function () {


return chartCtrl.showContextMenu;


}, function () {


scope.showContextMenu = chartCtrl.showContextMenu;


});


*/



function demandHasRelatedEntity() {


if (scope.point[scope.point.type])


return true;


else


return false;


}



return link(scope, element, attrs);


};



};



return $delegate;


});


});


})();



But is does not do what I want yet.



The idea is to override directive in order to not show context menu for bubbles. I assume it is dusplayed based on subscription to event: 'BUBBLE_CONTEXT_MENU_CLICKED'


therefore I need to disable reaction on that event for the directive as following:



scope.$on('BUBBLE_CONTEXT_MENU_CLICKED', function (evt, point) {


chartCtrl.showMenu(false);


});



instead of original:



scope.$on('BUBBLE_CONTEXT_MENU_CLICKED', function (evt, point) {


var actionLabel;


chartCtrl.showMenu(false);


demandPoints.fetchDemandData(point.sys_id).then(function (data) {


scope.point = data;


chartCtrl.showMenu(false);


$timeout(function () {


var menuWidth = getTextWidth();


scope.menuWidth = menuWidth + 10;


setupCoordinates();


chartCtrl.showMenu(true);


});


if (demandHasRelatedEntity()) {


actionLabel = viewLabel;


} else {


actionLabel = createLabel;


}


scope.menuOptions[0].name = actionLabel + " " + scope.point.label;


});


});


Another issue that I find with UI Script Overrides is that there are two possible ways:



1. UI Script - Global:true


2. UI Script - Global:false



the difference between them is in access to angular.



1. When UI Script Global = true, I access angular as following: top.angular().module


2. And When Global = false - Just angular().module ...



Anyway does not matter what is Global attribute - still want to nail down this challenge and the last code I have is:



(function() {



angular.module('sn.bubbleChartVisualization').config(function($provide) {



$provide.decorator('bubbleContextMenuDirective', function( $delegate ) {



var directive = $delegate[0]; // Get needed Directive from Array of directives


directive.priority = 1;


directive.terminal = true;



var link = function(scope, element, attrs,chartCtrl) {



var viewLabel = i18n.getMessage("View");


var createLabel = i18n.getMessage("Create");


var viewDemandLabel = i18n.getMessage("View Demand");



scope.menuWidth = 0;


scope.menuHeight = 20;


scope.cx = 0;


scope.cy = 0;


scope.point = {};


scope.showContextMenu = false;



scope.menuOptions = [{


name: viewLabel,


action: 'relatedEntityAction',


y: scope.menuHeight * 0


},


{


name: viewDemandLabel,


action: 'openDemand',


y: scope.menuHeight * 1


}];




/* OVERRIDING / DISABLING */



scope.$on('BUBBLE_CONTEXT_MENU_CLICKED', function (evt, point) {


chartCtrl.showMenu(false);


});






scope.doAction = function (name) {


if (name) {


scope[name]();


}


};



scope.openDemand = function () {


if (scope.point.sys_id) {


openWindow(DEMAND_FORM_URL + scope.point.sys_id + NOSTACK_URL_SNIPPET);


}


};



function openWindow(url) {


$window.open(url);


}



function getTextWidth() {


var elem = element.find('text');


return findMaxWidth(elem);


}



function findMaxWidth(elem) {


var largest = 0;


for (i = 0; i < elem.length; i++) {


if (coordinateSystem.elementWidth(elem[i], 135) > largest)


largest = coordinateSystem.elementWidth(elem[i], 135);


}


return largest;


}



function setupCoordinates() {


var menuXNeed = chartCtrl.cx + chartCtrl.bubbleRadius + scope.menuWidth;


var menuYNeed = chartCtrl.cy + chartCtrl.bubbleRadius + scope.menuOptions.length * scope.menuHeight;


if (menuXNeed > coordinateSystem.plotWidth) {


scope.cx = chartCtrl.cx - chartCtrl.bubbleRadius - scope.menuWidth - 10;


} else {


scope.cx = chartCtrl.cx + chartCtrl.bubbleRadius - 10;


}


if (menuYNeed > coordinateSystem.plotHeight) {


scope.cy = chartCtrl.cy - chartCtrl.bubbleRadius - scope.menuOptions.length * scope.menuHeight;


} else {


scope.cy = chartCtrl.cy + chartCtrl.bubbleRadius;


}


}



function createRelatedEntity(point) {


demandPoints.createRelatedEntity(point.sys_id, point.type).then(function (relatedEntityObj) {


var relatedEntityFormUrl = getRelatedEntityFormUrl(relatedEntityObj.sys_id, point.type);


var relatedEntityNumber = relatedEntityObj.number;


var relatedEntityTypeLabel = relatedEntityObj.label;


var link = '<a id="entity_creation_link" target="_blank" href="' + relatedEntityFormUrl + '">' + relatedEntityNumber + "</a> ";


var msg = i18n.getMessage("Demand create message");


var notification;


msg = msg.replace("{0}", relatedEntityTypeLabel);


msg = msg.replace("{1}", link);


notification = {


type: 'info',


message: msg


};



$rootScope.$broadcast('showNotification', notification);


$rootScope.$broadcast('refreshList');


});


}



function openRelatedEntity(point) {


openWindow(getRelatedEntityFormUrl(point[point.type], point.type));


}



function getRelatedEntityFormUrl(sysId, type) {


return demandPoints.relatedEntitiesConfig[type]['form'] + '?sys_id=' + sysId + NOSTACK_URL_SNIPPET;


}



scope.relatedEntityAction = function () {


if (demandHasRelatedEntity())


openRelatedEntity(scope.point);


else


createRelatedEntity(scope.point, scope.point.type);


scope.showContextMenu = false;


};



// OVERRIDE


/*


scope.$watch(function () {


return chartCtrl.showContextMenu;


}, function () {


scope.showContextMenu = chartCtrl.showContextMenu;


});


  */



function demandHasRelatedEntity() {


if (scope.point[scope.point.type])


return true;


else


return false;


}



return link(scope, element, attrs);


};



directive.compile = function() {


return function(scope, element, attrs) {


link.apply(this, arguments);


};


};




return $delegate;


});


});


})();


The "Global" checkbox is if the UI script is going to run globally for the native interface.



To associate it to Service Portal you would need to bring the UI Script in as a js include so that it will be applied to the particular portal. In the example below I'm using the "Stock" theme to bring in the UI Script for the "sp" portal (the OOB default portal)



1) Go to the theme of the desired portal it's needed for


theme.png


2) In the related link click on the JS Include tab


3) Click on the "New" button since it's never been added as a js include before



new.png



4) Fill in a name and select your UI Script



creating.png



5) Now it should be applied to your portal.


added.png


So, everything looks good so far but the main challenge right now is to make this UI Script load on OOTB Demand Workbench (Helsinki)


Andrii,



I got my hands on a Helsinki dev instance. I installed the plugins that bring in the Demand Workbench. And I apologize but I didn't know that the Demand Workbench really isn't a Service Portal page. I assumed that it was because of the angular script you presented. So, I started digging more into to see what I can learn about it.



It looks to me that this is a UI Page that is only accessible on the ServiceNow side of things where only ServiceNow employees can touch the actual code. Or it could be generated from a Processor. Knowing the ServiceNow Platform you can usually override parts such as UI pages, UI macros and UI scripts by creating the same type within your instance with the same name. Then when the system runs it will use your custom page/macro/script instead of the OOB.



On the instance I was using I couldn't find the UI Script holding the angular script (sn.bubbleChartVisualization). So, I couldn't create a UI Script with the same name because I didn't know the actual name of the UI Script. I ended up taking a different route. I created a UI page with the script taken from the $demand_workbench page (recovered via Inspect Element > Sources click on $demand_workbench.do?.... ). I named the UI page demand_workbench.



I gave it a try and looked like everything worked as normal as I tried all the features comparing to the OOB $demand_workbench page. Then I created a UI script with the script you posted to disable the bubble context menu. Brought that UI script into the UI page (<script></script>) and that seemed to work; meaning the bubble context menu was disabled.



Below is the screenshot of the custom demand workbench page:


demand_workbench.png



Keep in mind I didn't do a thorough test of it. But I thought it might give you some ideas of what can be done. That whole process really took about 10min since all of the code was already done.


I hope this helps in some way.



Here's a screencast overview of what I did. (Excuse the recording. It's not the best)


Custom Workbench UI Page