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 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 )){

Get URL Parameter - server side script (portal or classic UI)

Classic UI : var sURL_editparam = gs . action . getGlideURI (). getMap (). get ( ' sysparm_aparameter ' ); if ( sURL_editparam == 'true' ) { gs . addInfoMessage ( 'parameter passed ); } Portal : var sURL_editparam = $sp . getParameter ( " sysparm_aparameter " ); if ( sURL_editparam == 'true' ) { gs . addInfoMessage ( 'parameter passed ); }