- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎10-04-2017 06:26 AM
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?
Solved! Go to Solution.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎10-08-2017 08:55 AM
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:
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)
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎10-05-2017 04:17 AM
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:
- Overriding Directive Definitions In AngularJS
- [AngularJS] Overriding directives with a decorator....A Pen by Nick LaRosa. · GitHub
- javascript - Override a directive's link function - Stack Overflow
- Experiment: decorating directives
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;
});
});
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎10-05-2017 05:44 AM
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;
});
});
})();
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎10-05-2017 06:12 AM
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
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
4) Fill in a name and select your UI Script
5) Now it should be applied to your portal.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎10-06-2017 03:04 AM
So, everything looks good so far but the main challenge right now is to make this UI Script load on OOTB Demand Workbench (Helsinki)
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎10-08-2017 08:55 AM
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:
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)