Skip to main content

ServiceNow Service Portal: a 'my orders' widget

 ServiceNow Service Portal: a 'my orders' widget




Contents

HTML. 1

CSS. 1

Client 1

Server. 1


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

Popular posts from this blog

ServiceNow timeout settings for virtual agent chat

  After connecting to live agent User is connected to live agent and not responding to live agent chat or distracted. The below properties controls the timeout settings, the OOB settings for reminder is [180 sec] and for cancelling the chat is [360 sec]. The job is default configured to 2 min so I believe no tweaking is required here.  Property -   com.glide.cs.idle_chat_reminder_timeout                  com.glide.cs.idle_chat_cancel_timeout Scheduled job   - Idle Chat Timer Task   https://community.servicenow.com/community?id=community_article&sys_id=1453b03bdbaad0109e691ea668961929   (ServiceNow ) 

ServiceNow check for null or nil or empty (or not)

Haven't tested these all recently within global/local scopes, so feel free to have a play! option 1 use an encoded query embedded in the GlideRecord , e.g.  var grProf = new GlideRecord ( 'x_cls_clear_skye_i_profile' ); grProf . addQuery ( 'status=1^ owner=NULL ' ); grProf . query (); even better use the glideRecord  addNotNullQuery or addNullQuery option 2 JSUtil.nil / notNil (this might be the most powerful. See this link ) example: if ( current . operation () == 'insert' && JSUtil . notNil ( current . parent ) && ! current . work_effort . nil ())  option 3 there might be times when you need to get inside the GlideRecord and perform the check there, for example if the code goes down 2 optional routes depending on null / not null can use gs.nil : var grAppr = new GlideRecord ( 'sysapproval_approver' ); var grUser = new GlideRecord ( 'sys_user' ); if ( grUser . get ( 'sys_id' , current . approver )){...