Approvals History Simple List, New Widget - Query

ToniOxley
Kilo Expert

Hiya Guys, 

I have created a new widget on our "approvals" portal page to display a list of "My Recent Approval History (Last 15 Days)". 

Currently the list looks like this...

find_real_file.png

My issue is that none of the links work on the ""My Recent Approval History (Last 15 Days)" widget. 

Is there a way to remove the links from this list altogether so that they just display the refs in grey, and is there a way to add more information to the grey text below like the name of the request that they approved/rejected.

Please can you advise?

Thank you

find_real_file.png

find_real_file.png

find_real_file.png

My "Approvals Hist Simple List" widget linked to the "My Recent Approval History (Last 15 Days)" report on the portal screen shot above

HMTL BODY:

<div class="panel panel-{{::c.options.color}} b" ng-if="c.data.isValid && (c.options.always_show == 'true' || c.options.always_show == true || c.data.filterText || c.data.list.length)">
<div class="panel-heading" ng-if="::!c.options.hide_header">
<h2 class="h4 panel-title">
<span ng-if="c.options.glyph">
<fa name="{{::c.options.glyph}}" />
</span>{{::c.options.title}}</h2>
<!-- <i class="fa fa-filter" ng-click="c.toggleFilter()" ng-class="{'disabled-filter': !c.showFilter}"></i> -->
<div ng-show="c.showFilter">
<input aria-label="${Filter}" ng-model="c.data.filterText" ng-model-options="{debounce: 300}" sn-focus="c.showFilter" placeholder="{{::data.filterMsg}}" ng-change="c.update()" class="form-control input-sm filter-box">
</div>
</div>
<ul class="list-group hide-x-overflow" ng-style="::{maxHeight: c.getMaxHeight()}" style="overflow-y: auto;">
<li ng-if="c.data.list.length > 0" ng-repeat="item in c.data.list track by item.sys_id" class="list-group-item">
<a ng-click="c.onClick($event, item, item.url, {})" href="javascript:void(0)" oncontextmenu="return false;">
<span ng-repeat="action in c.data.actions" href="" ng-click="c.onClick($event, item, action.url, action)" ng-if="action.glyph"
class="list-action l-h-40 pull-right">
<fa name="{{action.glyph}}" ng-class="c.getActionColor(action)" />
</span>
<span ng-if="c.options.image_field" class="pull-left m-r"
ng-class="{'avatar': c.options.rounded_images, 'thumb-sm': c.options.rounded_images}">
<img ng-src="{{item.image_field}}" alt="..." class="img-sm" ng-class="{'img-circle': c.options.rounded_images}">
</span>
<div>
<div ng-switch on="item.display_field.type" ng-class="{'l-h-40': !item.secondary_fields.length}">
<span class="translated-html" ng-switch-when="translated_html" ng-bind-html="item.display_field.value"></span>
<div ng-switch-default>{{item.display_field.display_value}}</div>
</div>
<small class="text-muted" ng-repeat="f in item.secondary_fields">
<span ng-if="!$first"> • </span>
<span ng-switch="f.type" title="{{::f.label}}">
<span ng-switch-when="glide_date">
<span ng-if="!f.isFuture"> <sn-day-ago date="::f.value" /> </span>
<span ng-if="f.isFuture"> {{f.display_value}}</span>
</span>
<span ng-switch-when="glide_date_time">
<span ng-if="!f.isFuture"> <sn-time-ago timestamp="::f.value" /></span>
<span ng-if="f.isFuture"> {{f.display_value}}</span>
</span>
<span ng-switch-default="">{{f.display_value}}</span>
</span>
</small>
</div>
</a>
</li>
<div ng-if="!c.data.list.length" class="list-group-item">
${No records found}
</div>
</ul>
<div class="panel-footer" ng-if="!c.options.hide_footer && c.options.maximum_entries && c.data.count > c.options.maximum_entries">
<div class="h4 number-shown-label">{{c.getMaxShownLabel(c.options.maximum_entries, c.data.count)}}</div>
<a class="pull-right" ng-href="?id={{c.seeAllPage}}&table={{c.options.table}}&filter={{c.options.filter}}{{c.targetPageID}}">${View all}</a>
</div>
</div>

CSS:

.panel {
position: relative;
}

.panel-heading i {
cursor: pointer;
position: absolute;
padding: 10px;
top: 0px;
right: 0px;
cursor: pointer;
}

.disabled-filter {
color: #A0A0A0;
}

.list-group-item.ng-enter {
transition: all 1s;
-webkit-transition: all 1s;

background-color: #c0dcfa;
}

.list-group-item.ng-enter-active {
background-color: #fff;
}

.hide-x-overflow {
overflow-x: hidden;
}

.translated-html > p {
margin: 0px;
padding: 0px;
}

IMG {
max-width: 320px;
max-height: 240px;
}

IMG.img-sm {
max-height: 40px;
max-width: 40px;
}

.filter-box {
margin-top: 10px;
}

.panel-footer {
.number-shown-label {
margin-top: 0;
margin-bottom: 0;
font-size: 16px;
display: inline-block;
}

a {
color: inherit;
}
}

.list-group-item > a {
display: inline-block;
}

Server Script:

(function () {
data.filterMsg = gs.getMessage("Filter...");
if(gs.nil(options.hide_footer))
options.hide_footer = false;
options.hide_footer = (options.hide_footer == "true" || options.hide_footer == true);
options.table = $sp.getParameter('t') || options.table;
if (!options.table)
return;

var gr = new GlideRecordSecure(options.table); // does ACL checking for us
if(!gr.isValid()) {
data.isValid = false;
return;
} else
data.isValid = true;
// grTemp is used to check isValidField since using GlideRecordSecure fails for date/time fields
var grTemp = new GlideRecord(options.table);
gr.addEncodedQuery(options.filter);
options.title = options.title || gr.getPlural();
options.display_field = $sp.getParameter('f') || options.display_field;
if (!options.display_field || !grTemp.isValidField(options.display_field))
options.display_field = gr.getDisplayName();

if (input && input.filterText) {
gr.addEncodedQuery(options.display_field + "LIKE" + input.filterText)
}

options.title = options.title || gr.getPlural();
options.secondary_fields = options.secondary_fields || "";
options.secondary_fields = options.secondary_fields.split(",");
if (!options.order_by || !grTemp.isValidField(options.order_by))
options.order_by = options.display_field;

// Set ID of sp_page from option schema
if (options.list_page) {
var sp_page = GlideRecord('sp_page');
if (sp_page.get(options.list_page))
options.list_page_dv = sp_page.getDisplayValue('id');
}

// redo query with limit
if (options.order_direction == "asc")
gr.orderBy(options.order_by);
else
gr.orderByDesc(options.order_by);

data.maxCount = 500;
gr.setLimit(data.maxCount);
gr.query();

data.count = gr.getRowCount();
data.actions = getActions();
data.list = [];
var recordIdx = 0;
while (gr.next()) {
if (options.maximum_entries && recordIdx == options.maximum_entries)
break;

var record = {};
if (data.actions.length > 0) {
var fields = gr.getFields();
for (var i = 0; i < fields.size(); i++) {
var glideElement = fields.get(i);
var name = glideElement.getName();
if (name.indexOf("sys_") == -1)
record[name] = gr.getValue(name);
}
}

record.sys_id = gr.getValue('sys_id');
record.className = gr.getRecordClassName();
if (options.image_field) {
record.image_field = gr.getDisplayValue(options.image_field);
if (!record.image_field)
record.image_field = "noimage.pngx";
}

if (options.display_field)
record.display_field = getField(gr, options.display_field);

record.secondary_fields = [];
options.secondary_fields.forEach(function(f) {
record.secondary_fields.push(getField(gr, f));
});
if (options.sp_page) {
var view = "sp";
if (options.view) {
var viewGR = new GlideRecord("sys_ui_view");
viewGR.get(options.view);
view = viewGR.getValue("name");
}
record.url = {id: options.sp_page, table: record.className, sys_id: record.sys_id, view: view};
} else if (options.url != '')
record.url = options.url;
else
record.url = null;

data.list.push(record);
recordIdx++;
}


function getField(gr, name) {
var f = {};
f.display_value = gr.getDisplayValue(name);
f.value = gr.getValue(name);
var ge = gr.getElement(name);
if (ge == null)
return f;

f.type = ge.getED().getInternalType();
if (f.type == "glide_date_time")
f.isFuture = gs.dateDiff(gr.getValue(name), gs.nowNoTZ(), true) < 0;
else if (f.type == "glide_date")
f.isFuture = gs.dateDiff(gr.getValue(name), gs.now(), true) < 0;
f.label = ge.getLabel();
return f;
}

function getActions() {
var rl = GlideRecord("sp_vlist_action");
rl.addQuery("sp_rectangle_vlist",$sp.getValue("sys_id"));
rl.query();
var actions = [];
while(rl.next()) {
var action = {};
$sp.getRecordValues(action, rl, "name,glyph,hint,url,color");
actions.push(action);
}
return actions;
}
})()

Client Controller

function ($scope, $location, $rootScope, spUtil, $interpolate) {
var c = this;

this.data.filterText = "";
this.showFilter = false;

this.onClick = function($event, item, url, action) {
$event.stopPropagation();
$event.preventDefault();
if (typeof url == "string") {
var urlExp = $interpolate(url);
url = urlExp(item);
$location.url(url);
} else if (url && typeof url == "object")
$location.search(url);
else {
var evt = {};
evt.url = url;
evt.table = item.className;
evt.sys_id = item.sys_id;
evt.record = item;
evt.rectangle_id = c.options.sys_id;
evt.action = action;
// put out the selection with simple list "sl_" prefix
$location.search('sl_sys_id', evt.sys_id);
$location.search('sl_table', evt.table);
$location.search('spa', 1); // spa means "I've got this"
$rootScope.$broadcast('$sp.list.click', evt);
}
};

if (c.options.table && c.options.disable_record_watcher != 'true')
spUtil.recordWatch($scope, c.options.table, c.options.filter);

this.getMaxShownLabel = function(maxEntries, totalCount) {
if (totalCount == c.data.maxCount)
return "${First [0] of more than [1]}".replace('[0]', maxEntries).replace('[1]', totalCount);

return "${First [0] of [1]}".replace('[0]', maxEntries).replace('[1]', totalCount);
};

this.seeAllPage = c.options.list_page_dv || 'list';
this.targetPageID = (c.options.sp_page) ? "&target_page_id=" + c.options.sp_page : "";

c.getMaxHeight = function() {
return c.options.panel_body_height || 'none';
};

c.getActionColor = function(action) {
return "text-" + action.color;
};

c.update = function update() {
c.server.update();
}

c.toggleFilter = function() {
c.showFilter = !c.showFilter;
}

}

1 ACCEPTED SOLUTION

Okay,

HTML body change below line

<a ng-click="c.onClick($event, item, item.url, {})" href="javascript:void(0)" oncontextmenu="return false;">

to

<a oncontextmenu="return false;">

This should give you expected result.

View solution in original post

4 REPLIES 4

rahulpandey
Kilo Sage

Hi, 

I could recreate the widget and links are working fine for me. Below is what I did.

Note: to achieve this, you do not need to create/ clone widget.

- I opened the page in designer by pressing CTRL+right click together on any widget of page. like below

find_real_file.png

 

- Dragged simple list widget below the approval widget and clicked on edit icon.

find_real_file.png

Here you can set all the needed options. Link to the page, where your links will navigate to.

find_real_file.png

Additionally,

You can go to widget on page and click ctrl+right click, you will see the same option window.

To set the title, you will have to visit sp_instance table and set title there.

 

Please mark correct answer if this helps.

Sorry that doesn't seem to have done anything.. still nothing happens when i click on the links. Also I am trying to remove the links anyway and have them greyed out ideally. Thank you though for the response.

Okay,

HTML body change below line

<a ng-click="c.onClick($event, item, item.url, {})" href="javascript:void(0)" oncontextmenu="return false;">

to

<a oncontextmenu="return false;">

This should give you expected result.

michaelward
Tera Guru

I know there's an accepted solution there, but there was no mention of using the browser's dev tools (the 'inspect on a right-click', the debugging strategies within ServiceNow for determining why the links don't work (using the <pre> tag in html to output objects; console.log, gs.log, spUtil.addInfoMessage, etc.), or cloning strategies.  As described in a reply, this seemed like it could be solved in the simple list widget, but sometimes you do have to clone to get where you need to go.   I landed here looking for information on widget instance list actions (sp_vlist_action table), and that might even be in the ballpark for the solution here. Take a look at the following: 

https://serviceportal.io/simple-list-widget/

Anyway, my two cents.  Peace.