ServiceNow Service Portal: a 'my orders' widget
Contents
HTML
<div class="container-fluid">
<h1>
{{::c.options.title}}
</h1>
<div class="panel panel-success" ng-class="{'hide-border' :
c.options.panel_border!='true'}">
<div class="panel-heading">
<div class="pull-left panel-button-row">
<a class="btn btn-lg btn-primary" ng-class="{active :
data.filter=='active'}" ng-href="?id={{data.currentPage}}&filter=active" role="button">{{::c.options.active_button_label}}</a>
<a class="btn btn-lg btn-primary" ng-class="{active :
data.filter!='active'}" ng-href="?id={{data.currentPage}}&filter=inactive" role="button">{{::c.options.inactive_button_label}}</a>
</div>
<div class="pull-right search-bar">
<widget id="typeahead-search" options=data.search_options></widget>
</div>
<div class="clearfix"></div>
</div>
<div class="panel-body">
<div class="list-header">
<div class="stats pull-left">
<h4>{{::data.msgs.total_label}}
<div ng-if="data.filter=='active'" class="button-group day-dropdown-container" uib-dropdown is-open="dayDropdownStatus.isopen">
<button id="day-dropdown" type="button" class="btn btn-primary" uib-dropdown-toggle ng-disabled="disabled">
{{c.dayRange}} <i class="fa fa-chevron-down"></i>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="day-dropdown">
<li role="menu-item"><a ng-hide="c.data.daysAgo == '7'" ng-href="?id={{data.currentPage}}&daysago=7{{c.getSortParam()}}{{c.getFilterParam()}}">Last 7 days</a></li>
<li role="menu-item"><a ng-hide="c.data.daysAgo == '30'" ng-href="?id={{data.currentPage}}&daysago=30{{c.getSortParam()}}{{c.getFilterParam()}}">Last 30 days</a></li>
</ul>
</div>
</h4>
</div>
<ul class="nav nav-pills pull-right">
<li><h4>${Sort by}:</h4></li>
<li role="presentation" ng-class="{'active':
data.sort==='sys_updated_on'}"><a ng-href="?id={{data.currentPage}}&sort=sys_updated_on{{c.getFilterParam()}}{{c.getDaysAgoParam()}}">{{::data.msgs.last_updated}}</a></li>
<li role="presentation" ng-class="{'active': data.sort==='sys_created_on'}"><a ng-href="?id={{data.currentPage}}&sort=sys_created_on{{c.getFilterParam()}}{{c.getDaysAgoParam()}}">{{::data.msgs.newest}}</a></li>
<li role="presentation" ng-class="{'active':
data.sort==='status'}"><a ng-href="?id={{data.currentPage}}&sort=status{{c.getFilterParam()}}{{c.getDaysAgoParam()}}">{{::data.msgs.status}}</a></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="list-body">
<uib-accordion close-others="true">
<uib-accordion-group
ng-class="['accordion-header']"
template-url="group-template.html"
ng-repeat="record in c.data.recordList track by record.sys_id"
id="{{::record.sys_id}}"
is-open="record.isOpen">
<uib-accordion-heading>
<div class="row">
<div class="flex-container">
<div aria-hidden="true" id="record_name_{{::record.sys_id}}" class="description-wrapper">
<img ng-if="record.icon"ng-src="{{::record.icon}}" class="item-icon pad-right"/>
<span aria-role="heading" class="description">{{::record.short_description}}</span>
</div>
<div aria-hidden="true" class="stage-container">
${{{::data.msgs.current_state}}}: <span class="stage-value" ng-class="{'positive-stage':
data.filter=='active'}">{{record.stage}}</span>
<span class="fa accordion-icon" ng-class="record.isOpen ? 'fa-chevron-up'
: 'fa-chevron-down'" title="{{record.isOpen
?'${Collapse}':'${Expand}'}}" aria-hidden="true"></span>
</div>
</div>
<div ng-if="record.additionalFields.length
> 0" class="sub-heading">
<span class="additional-field" ng-repeat="addition in
record.additionalFields">
<span class="field-label">{{addition.field_label}}:</span>
<span class="field-value">{{addition.field_value}}</span>
</span>
</div>
</div>
</uib-accordion-heading>
<div id="record_details_{{::record.sys_id}}" role="region" aria-labelledby="{{::record.sys_id}}">
<div class="wrapper record-details-wrapper">
<div class="panel-body">
<span>${Reference}: {{record.number}}</span><br/>
<span>${{{::data.msgs.current_state}}}:
{{record.stage}}</span><br/>
<span ng-if="record.item_name">${Item}: {{record.item_name}}</span>
</div>
<a class="btn btn-large btn-block btn-primary" role="button" ng-href="?id={{::c.data.targetPage}}&sys_id={{::record.sys_id}}&table={{::c.data.table}}">${{{c.options.view_record_label}}}</a>
</div>
</div>
</uib-accordion-group>
</uib-accordion>
<div class="show-more" ng-if="data.recordCount >
data.showingCount">
<a ng-href="?id=list&t={{data.table}}&filter={{data.activeQuery}}&target_page_id={{data.targetPage}}&o={{(data.sort=='status')
? data.stateField : data.sort}}&d=desc">{{data.msgs.click_more_label}}<br/><i class="fa fa-chevron-down"></i></a>
</div>
</div> <!-- .list-body -->
</div> <!-- .panel-body -->
</div> <!-- .panel -->
</div> <!-- .container-fluid -->
<script type="text/ng-template" id="group-template.html">
<div class="accordian-panel">
<div class="panel-heading">
<div class="panel-title">
<div tabindex="0" class="accordion-toggle" ng-click="toggleOpen()" uib-accordion-transclude="heading" role="none">
<span uib-accordion-header>
</span>
</div>
</div>
</div>
<div class="panel-collapse collapse" uib-collapse="!isOpen">
<div ng-transclude></div>
</div>
</div>
</script>
CSS
/* Use with ng-class to selectively use
in a theme */
.hide-border {
border: none;
}
/* Blending in the buttons with the
panel-header */
.panel-button-row a.btn-primary.active
{
background-color: #fff;
color: $brand-success;
}
/* Style the search bar nicely */
.search-bar {
width: 380px;
input[name="q"] {
border-top-left-radius:$search-border-radius;
border-bottom-left-radius:$search-border-radius;
color: $input-color !important;
}
span.input-group-btn {
button.btn-default {
border-top-right-radius:$search-border-radius;
border-bottom-right-radius:$search-border-radius;
border-left: none;
color: $brand-success;
}
}
input[name="q"] {
&::placeholder {
color: $input-color;
}
&::-ms-input-placeholder {
color: $input-color;
}
&:-ms-input-placeholder {
color: $input-color;
}
&::-webkit-input-placeholder {
color: $input-color;
}
&:-moz-placeholder {
color: $input-color;
}
&::-moz-placeholder {
color: $input-color;
}
}
}
/* Format the tabs into pills and other
header stuff */
.list-header {
padding-left: 1em;
padding-right: 1em;
margin-bottom: 1em;
&>.nav-pills > li + li {
margin-left:1em;
}
&>.nav-pills > li {
font-size: 12px;
}
&>.nav-pills > li > h4 {
padding: 5px;
}
&>.nav-pills > li > a {
border-radius: $search-border-radius;
background-color: $search-border-color;
color: #000;
padding: 7px 12px;
top: 10px;
&:hover {
background-color: darken($search-border-color, 50%); /* Calculate a
darker colour */
color: #fff;
}
}
&>.stats {
margin-top: 10px;
margin-bottom: 10px;
padding: 5px;
h4 {
color: #000;
display: inline;
.day-dropdown-container {
display:inline-block;
button {
margin-left: 5px;
background: $search-border-color;
height: 42px;
border:none;
color: #000;
.fa {
margin-left: 5px;
}
}
.dropdown-menu > li > a {
color: #000;
}
}
}
}
&>.nav-pills > li.active > a {
background-color: $brand-success;
color: #fff;
}
}
.item-icon {
max-width:30px;
height:auto;
}
.positive-stage {
color: $brand-info;
}
.accordian-panel {
background-color: #F2FBFE; /* TODO: Make into a CSS variable in the
Theme */
border: none;
margin-bottom: 15px;
padding: 10px;
border-radius: $border-radius-base;
/* Using flex to distribute icons in accordian-header */
.flex-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
height:unset;
width: unset;
.description-wrapper {
min-width: 250px;
}
span.description {
font-weight: 600;
color: $brand-primary;
text-decoration: underline;
}
.stage-container .fa {
margin-left: 10px;
color: $search-border-color;
}
.stage-container .stage-value {
font-weight: 600;
}
}
/* These are the contents of each panel (stage and button to view
details) */
.record-details-wrapper {
background-color: #fff;
position: relative;
border-radius: $border-radius-large;
margin-top: 10px;
}
.sub-heading {
font-size: smaller;
.additional-field {
margin-right:5px;
.field-label {
font-weight: bold;
}
}
}
}
.show-more {
text-align: center;
a {
display:block;
}
}
.accordion-toggle {
padding: 17px;
&:focus {
outline-offset: 0;
}
}
Client
function($scope, spUtil, $timeout) {
/* widget controller */
var c = this;
$timeout(function() {
spUtil.setBreadCrumb($scope, [
{label: c.data.myOrdersBreadcrumb, url : '#'}
]);
});
c.getFilterParam = function() {
return (c.data.filter) ? "&filter=" + c.data.filter : "";
}
c.getSortParam = function() {
return (c.data.sort) ? "&sort=" + c.data.sort : "";
}
c.getDaysAgoParam = function() {
return (c.data.daysAgo) ? "&daysago=" + c.data.daysAgo : "";
}
// Dynamic binding to show/hide the day options
c.dayDropdownStatus = {
isopen: false
}
// Dynamic binding used for the day
dropdown label
c.dayRange = "Last " + c.data.daysAgo + " days";
}
Server
(function() {
// Prepare messages for template
data.msgs = {};
data.msgs.last_updated = gs.getMessage("Last updated");
data.msgs.newest = gs.getMessage("Created");
data.msgs.status = gs.getMessage("Status");
data.msgs.current_state = options.current_state_label || "Current State";
data.msgs.click_more_label = gs.getMessage(options.click_more_label);
data.myOrdersBreadcrumb = gs.getMessage(options.title);
data.search_options = "{title:'Search open orders', size: 'md', color: 'default',
contextual_search_sources: '3d9d9d0edb8d6c10f04aad0505961925'}";
// Ensure to redirect to current or specified page when sorting
data.currentPage = $sp.getParameter("id");
data.stateField = options.query_state_field || 'state';
var sortTypes = ['sys_updated_on', 'sys_created_on', 'status'];
data.sort = (input && input.sort) || $sp.getParameter("sort") || "sys_updated_on";
if (!contains(sortTypes, data.sort))
data.sort = "sys_updated_on";
var filterTypes = ['active', 'inactive'];
data.filter = (input && input.filter) || $sp.getParameter("filter") || "active";
if (!contains(filterTypes, data.filter)) {
data.filter = "active";
}
var daysAgoOptions = ['7', '30'];
data.daysAgo = (input && input.daysAgo) || $sp.getParameter('daysago') || "7";
if (!contains(daysAgoOptions, data.daysAgo)) {
data.daysAgo = "7";
}
var recordList = [];
data.table = options.query_table || "incident";
data.queryLimit = options.query_limit || 5;
data.targetPage = options.target_page || "ticket";
data.activeQuery = options.active_query || "active=true";
data.inActiveQuery = options.inactive_query || "active=false";
// Prep date range query string
var sDaysAgo = "", nDaysAgo = parseInt(data.daysAgo);
switch (nDaysAgo) {
case 7 :
sDaysAgo = "^sys_created_onONLast 7
days@javascript:gs.beginningOfLast7Days()@javascript:gs.endOfLast7Days()";
break;
case 30 :
sDaysAgo = "^sys_created_onONLast 30
days@javascript:gs.beginningOfLast30Days()@javascript:gs.endOfLast30Days()";
break;
default:
// TBD
}
var grRecord = new GlideRecord(data.table);
if (data.filter=='active') {
grRecord.addEncodedQuery(data.activeQuery + sDaysAgo);
}
else {
grRecord.addEncodedQuery(data.inActiveQuery);
}
grRecord.setLimit(data.queryLimit);
if (data.sort=='status') {
grRecord.orderByDesc(data.stateField);
}
else {
grRecord.orderByDesc(data.sort);
}
grRecord.query();
while (grRecord.next()) {
var targetRecord = {
number: grRecord.getDisplayValue(),
sys_id: grRecord.getUniqueValue(),
short_description: grRecord.getValue('short_description')
};
targetRecord.stage = grRecord.getDisplayValue(data.stateField);
/* Catalogue specific attempt at getting an image for the list */
if (grRecord.cat_item && grRecord.cat_item.icon.toString() != '') {
targetRecord.icon = grRecord.cat_item.icon.toString() + ".iix"
}
if (grRecord.cat_item && grRecord.cat_item.icon.toString() == '' && grRecord.cat_item.picture.toString() != '') {
targetRecord.icon = grRecord.cat_item.picture.toString() + ".iix"
}
// Check if there are additional fields specified in the options and
show them in the sub-header
targetRecord.additionalFields = [];
if (options.additional_fields != '') {
var additionalFields = options.additional_fields.split(',');
for (var a = 0; a < additionalFields.length; a++) {
var sField = additionalFields[a];
var oField = {
'field_label' : grRecord[sField].getLabel(),
'field_value' : grRecord.getDisplayValue(sField)
}
targetRecord.additionalFields.push(oField);
}
}
recordList.push(targetRecord);
}
data.recordList = recordList;
data.showingCount = recordList.length;
data.recordCount = getCount(data.table, (data.filter=='active') ? data.activeQuery+sDaysAgo : data.inActiveQuery);
if (data.filter=='active') {
data.msgs.total_label = gs.getMessage("{0} orders placed in", [data.recordCount])
}
else {
data.msgs.total_label = (options.total_inactive_label || "Total closed records") + ": " + data.recordCount;
}
})();
function contains(arr, str) {
for (var i = 0; i < arr.length; i++) {
if (arr[i].equals(str))
return true;
}
return false;
}
function getCount(table, query) {
var count = 0,
ga = new GlideAggregate(table);
ga.addAggregate('COUNT');
ga.addEncodedQuery(query);
ga.query();
if (ga.next()) {
count = ga.getAggregate('COUNT');
}
return count;
}
Comments
Post a Comment