AndersBGS
Tera Patron

When you install the Service Operation Workspace on a instance, you notice that contact cards are available on the contextual side menu.

 

For incidents "requester" and "Assigned to" is available, and for requested items "requested for" and "opened by" is available out of the box. But what if (as in our circumstance) we have created some custom fields back in the days e.g. for incidents we have the field "Requester" = "caller_id" and End "End user" = "u_end_user"?

In this article, I will go through how we can create an additional contact card in the Service Operation workspace for incidents end user.

 

Understand the dataflow

First of all, we need to understand how that data is populated into the SOW. If we go to the record https://instance.service-now.com/now/builder/ui/edit/pc/728ec88c43fa2110f20fff53e9b8f278/e934b8dc430... and look at the "caller card" component as shown below, we can see that the information for e.g. "caller card" is receiving its data from the data ressource called "record_information".

 

AndersBGS_0-1794223566525.png

AndersBGS_1-1794223576240.png

We can then open the record in core ui

AndersBGS_2-1794223586640.png

 

 

Where the script will reweal that it looks towards the script include "SOWIncidentInfo". Then when opening the script include, it reweal that it looks towards another script include called "SOWIncidentInfoSNC". As we now have located the data source for the contact card we can start develop.


Duplicate variant and componets

Step 1.
To start with, create a global scope update set for all the development effort. This will function as the parent for additional update sets. Afterwards, change scope to "Service Operations Workspace ITSM Common" and create a new update set where parent is the first created.

 

Step 2:
Go to the URL and duplicate the variant within UI Builder: "now/builder/ui/edit/pc/728ec88c43fa2110f20fff53e9b8f278/e934b8dc43032110361dff53e9b8f21c/afd4b8dc43032110361dff53e9b8f28a" and duplicate the variant, as the current variant is read only.

 

AndersBGS_3-1794223612873.png

step 3
Open the new varian created and duplicate the "caller Details" component together with child component:

AndersBGS_4-1794223624194.png

Step 4
Set the variant order to "0", as this then will be the preferred variant

 

Step 5
First we need to change application scope to " Incident Management for Service Operations Workspace" and create a new update set with parent set to the global update set that we created, as the script include located earlier is in this application scope.

 

Duplicate script include

Now we need to go to the script include located earlier called "SOWIncidentInfoSNC". As the item is read only based on its protection policy, we need to duplicate the script include.
- Copy all the code
- Create a new script include called e.g. SOWIncidentInfoSNCCustom" and past the code in.
- Remember to save
- Locate the function called "getSourceInfo", and replace below code:

getSourceInfo: function() {
var result = {};
result.showCallerInfo = false;
result.showOriginInfo = false;
if (!gs.nil(this._gr.caller_id)) {
result.showCallerInfo = true;
result.callerInfo = this.getCallerInfo();
}

if (!gs.nil(this._gr.opened_for && this._gr.getTableName() == 'interaction')) {
result.showCallerInfo = true;
result.callerInfo = this.getCallerInfo();
}

if (!gs.nil(this._gr.origin_table) && (this.SUPPORTED_ORIGIN_TABLES.indexOf(this._gr.origin_table.toString()) != -1)) {
result.showOriginInfo = true;
result.originInfo = this.getOriginInfo();
}
return result;
},

With below code and press save

getSourceInfo: function() {

var result = {};
result.showCallerInfo = false;
result.showOriginInfo = false;
if (!gs.nil(this._gr.caller_id)) {
result.showCallerInfo = true;
result.callerInfo = this.getCallerInfo();
}
result.showEndUserInfo = false;

if (!gs.nil(this._gr.opened_for && this._gr.getTableName() == 'interaction')) {
result.showCallerInfo = true;
result.callerInfo = this.getCallerInfo();
}

if (!gs.nil(this._gr.u_end_user)) {
result.showEndUserInfo = true;
result.endUserInfo = this.getEndUserInfo();
}

if (!gs.nil(this._gr.origin_table) && (this.SUPPORTED_ORIGIN_TABLES.indexOf(this._gr.origin_table.toString()) != -1)) {
result.showOriginInfo = true;
result.originInfo = this.getOriginInfo();
}
return result;
},

Now paste the below function after the previous function:


getEndUserInfo: function() {
var result = {};
var endUser = {};
var endUserRef = this._gr.u_end_user.getRefRecord();

endUser.sysId = endUserRef.getUniqueValue();
endUser.avatar = endUserRef.avatar ? "/" + endUserRef.avatar + ".iix?t=small" : "";
endUser.name = this._gr.getDisplayValue("u_end_user");
endUser.timeZone = gs.getProperty("glide.sys.default.tz") || 'America/Los_Angeles';
endUser.timeZoneValue = gs.getProperty("glide.sys.default.tz") || 'America/Los_Angeles';

if (endUserRef.title)
endUser.title = endUserRef.getDisplayValue("title");

if (endUserRef.department)
endUser.department = endUserRef.getDisplayValue("department");

var locationRef = endUserRef.location.getRefRecord();

if (locationRef && locationRef.city)
endUser.city = locationRef.getDisplayValue("city");

if (endUserRef.time_zone) {
endUser.timeZone = endUserRef.getDisplayValue("time_zone");
endUser.timeZoneValue = endUserRef.getValue("time_zone");
}

if (endUserRef.vip)
endUser.vip = [{
"label": gs.getMessage("VIP"),
"color": "critical"
}];

result.endUser = endUser;
result.secondaryInfo = this.getFieldsInfo(this.FIELDS.SOURCE);

return result;
},

After above change we afterwards needs to go to the other script include called "SOWIncidentInfo", and point it towards our new custom script include. change the code from:

 

var SOWIncidentInfo = Class.create();
SOWIncidentInfo.prototype = Object.extendsObject(sn_sow_inc.SOWIncidentInfoSNC, {
type: 'SOWIncidentInfo'
});

to below and press save

var SOWIncidentInfo = Class.create();
SOWIncidentInfo.prototype = Object.extendsObject(sn_sow_inc.SOWIncidentInfoSNCCustom, {
type: 'SOWIncidentInfo'
});

Now we need to change the scope again back to Service Operations Workspace ITSM Common and go back into UI bilder.

UI builder configuration

Create a new client script:

AndersBGS_5-1794224070291.png

 

Paste the following code and call it "EndUserContactField:

/**

* @param {params} params
* @param {api} params.api
* @param {any} params.event
* @param {any} params.imports
* @param {ApiHelpers} params.helpers
*/

function handler({api, event, helpers, imports}) {
var endUserField = {
...api.state.endUserField
};

var content = [];
const { NowDateTime } = imports['sn_uxf.core_libraries']();

if (api.data.record_information.output && api.data.record_information.output.sourceInfo) {
if (api.data.record_information.output.sourceInfo.endUserInfo.endUser.city)
content.push(api.data.record_information.output.sourceInfo.endUserInfo.endUser.city);

var timeZone = api.data.record_information.output.sourceInfo.endUserInfo.endUser.timeZone;
var timeZoneValue = api.data.record_information.output.sourceInfo.endUserInfo.endUser.timeZoneValue;
var timeFormat = api.context.session.user.timeFormat;

content.push(NowDateTime().tz(timeZoneValue).format(timeFormat) + ' ' + timeZone);
}

var subHeadingList = [];

if (api.data.record_information.output && api.data.record_information.output.sourceInfo) {
if (api.data.record_information.output.sourceInfo.endUserInfo.endUser.title)
subHeadingList.push(api.data.record_information.output.sourceInfo.endUserInfo.endUser.title);
if (api.data.record_information.output.sourceInfo.endUserInfo.endUser.department)
subHeadingList.push(api.data.record_information.output.sourceInfo.endUserInfo.endUser.department);
}

endUserField["contactField"] = [{
'label': subHeadingList.join(' · ')
},

{
"label": content.join(' · ')
}
];

if (api.context.props.recordTable == 'incident') {
const slaConfig = api.data.record_information.output.slaConfig;
if (slaConfig.length > 0) {

// Set config first because empty config means: First to breach

endUserField["slaTimerCfgSysId1"] = slaConfig[0].configId;
endUserField["slaTimerCfgSysId2"] = slaConfig.length > 1 ? slaConfig[1].configId : '';
endUserField["taskSysId"] = api.context.props.recordSysId;
}

const count = api.data.record_information.output.slaInfo.count;
endUserField['totalSLAs'] = count;
const viewMoreText = api.data.record_information.output.slaInfo.buttonText;
endUserField['slaViewMoreText'] = viewMoreText;
let slaViewMoreBtnARIAProps = {

"button": {
"aria-label": viewMoreText
}
};

endUserField['slaViewMoreBtnARIAProps'] = slaViewMoreBtnARIAProps;

}

var usersGrp = api.data.record_information.output.loggedInUserGroup;
var userhasGrp = false;

if(api.data.record_information.output.incidentInfo)
usersGrp.forEach(function(u) {
if(u.value === api.data.record_information.output.incidentInfo.assignmentGroup) userhasGrp = true;
});

endUserField['userhasGrp'] = userhasGrp;

var mimDuration = api.data.record_information.output.mimDuration;
if (mimDuration && !mimDuration.increment) {
endUserField["staticMIMDuration"] = msToTime(mimDuration.time);
}

endUserField["hideDialogLink"] = api.data.record_information.output.sourceInfo?.originInfo?.dialog?.isHidden;

endUserField["hideWorkNotes"] = api.data.record_information.output.sourceInfo?.originInfo?.workNotes?.isHidden;

api.setState('endUserField', endUserField);

function msToTime(duration) {
if (duration < 1000)
duration = 0;
let seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60),
hours = Math.floor((duration / (1000 * 60 * 60)) % 24),
days = Math.floor((duration / (1000 * 60 * 60)) / 24);
hours = ("0" + hours).slice(-2);
minutes = ("0" + minutes).slice(-2);
seconds = ("0" + seconds).slice(-2);

if (days)
return days + "d " + hours + "hr " + minutes + "min " + seconds + "sec";
else
return hours + "hr " + minutes + "min " + seconds + "sec";
}
}

In the client state parameters add the following:

AndersBGS_6-1794224105120.png

 

In the data ressource called "record information" under events, add the newly created client script

AndersBGS_7-1794224114369.png

 

Eventhandler

/**
* @param {params} params
* @param {api} params.api
* @param {any} params.event
*/

function evaluateEvent({api, event}) {
var subHeadingList = [];
if (api.data.record_information.output && api.data.record_information.output.sourceInfo) {
if (api.data.record_information.output.sourceInfo.endUserInfo.endUser.title)
subHeadingList.push(api.data.record_information.output.sourceInfo.endUserInfo.endUser.title);
if (api.data.record_information.output.sourceInfo.endUserInfo.endUser.department)
subHeadingList.push(api.data.record_information.output.sourceInfo.endUserInfo.endUser.department);
};

return {
viewportElementId: "viewport_glu",
route: "caller-contact",
fields: {
"table": api.context.props.recordTable,
"recordSysId": api.context.props.recordSysId,
"userSysId": api.data.record_information.output.sourceInfo.endUserInfo.endUser.sysId
},

params: {
"subTitle": subHeadingList.join(' · '),
"parent": "record-info-incident",
"isCtiEnabled": api.data.record.isCtiEnabled
}
};
}

Lastly, on every component that was created earlier, verify that it points towards the right field

AndersBGS_8-1794224134245.png

 

e.g. Container 6 points towards:

AndersBGS_9-1794224143127.png

 

or Highligted value 2 points towards:

AndersBGS_10-1794224151664.png

Also ensure that the components style match the OOTB components, otherwise the card would show very odd.

 

Hope above help if needed.

 

Anders


Version history
Last update:
6 hours ago
Updated by: