logo

NJP

Multiple Tickets types in Single Widget - My Tickets Page

Import · May 11, 2019 · article

I saw few questions about on how to show end user their tickets that are INC and REQ on same widget so I decided to write this article.

1. Create new widget called Name: My Ticket and ID: my-tickets

HTML:

<sp-panel>
  <uib-tabset>
    <uib-tab index="0" heading="Active Requests" uib-tooltip="View all active requests that you opened or that were opened on your behalf."><sp-widget widget="data.all_active"></sp-widget></uib-tab>
    <uib-tab index="1" heading="Closed Requests" uib-tooltip="View all closed requests that you opened or that were opened on your behalf."><sp-widget widget="data.all_closed"></sp-widget></uib-tab>
  </uib-tabset>
</sp-panel>

Server: (Change filter as you like)

(function(){

    // Setup data tables
    var activeFilter = 'sys_class_name=incident^state=1^ORstate=2^ORstate=4^ref_incident.caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe^ORopened_byDYNAMIC90d1921e5f510100a9ad2572f2b477fe^NQactive=true^sys_class_name=sc_request^ref_sc_request.requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe^ORopened_byDYNAMIC90d1921e5f510100a9ad2572f2b477fe';
    var taskQueryFilter = 'short_descriptionLIKE{q}^ORnumberLIKE{q}^ORrequested_for.nameLIKE{q}^ORu_user.nameLIKE{q}^ORref_incident.caller_id.nameLIKE{q}^ORdescriptionLIKE{q}';
    var all_active = {
        "table": "task",
        "view": "mobile",
        "fields": "number,short_description,sys_created_on,sys_updated_on",
        "show_keywords": true,
        "show_breadcrumbs": false,
        "o": "sys_created_on", // order_by
        "filter": activeFilter,
        "storef": activeFilter,
        "query_filter": taskQueryFilter
    };

    var closedFilter = 'active=false^sys_class_name=incident^ORsys_class_name=sc_request';
    var all_closed = {
        "table": "task",
        "view": "mobile",
        "fields": "number,short_description,sys_created_on,sys_updated_on",
        "show_keywords": true,
        "show_breadcrumbs": false,
        "o": "sys_created_on", // order_by
        "filter": closedFilter,
        "storef": closedFilter,
        "query_filter": taskQueryFilter
    };

    data.all_active = $sp.getWidget("my_widget_data_table", all_active);
    data.all_closed = $sp.getWidget("my_widget_data_table", all_closed);

    // Look for any parameters for setting the active tab
    data.tabName = $sp.getParameter('tab_focus') || 'all_active';

})();

Client:

function ($scope, $location, $rootScope, $uibModal, spUtil, $http) {
    var c = this;
    this.filterText = "";
    this.showFilter = false;

    $scope.$on('data_table.click', function(event, mass) {
        event.stopPropagation();
        event.preventDefault();     
        $location.search('id', 'ticket');
        $location.search('sys_id', mass.sys_id);
        $location.search('table', mass.table);  
    });
}

2. Create new widget called Name: My Data Table and ID: my_widget_data_table

HTML:

<div>
  <div class="panel panel-{{options.color}} b">
    <div class="panel-heading form-inline" ng-hide="options.hide_header">
      <span class="dropdown m-r-xs">
        <span class="dropdown-toggle glyphicon glyphicon-menu-hamburger" style="line-height: 1.4em" id="optionsMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"></span>
        <ul class="dropdown-menu" aria-labelledby="optionsMenu">
          <li ng-repeat="t in ::exportTypes">
            <a ng-href="/{{data.table}}_list.do?{{::t.value}}&sysparm_query={{data.filter}}&sysparm_view={{data.view}}" target="_new">${Export as} {{::t.label}}</a>
          </li>
        </ul>
      </span>
      <span class="panel-title"><i ng-if="options.glyph" class="fa fa-{{options.glyph}} m-r"></i>{{data.title || data.table_plural}}</span>

      <button name="new" type="button" class="btn btn-primary btn-sm m-l-xs" ng-click="newRecord()" ng-if="options.show_new && data.canCreate && !data.newButtonUnsupported">${New}</button>
      <div class="pull-right" ng-if="options.show_keywords">
        <form ng-submit="setSearch(true)">
          <div class="input-group">
            <input type="text" name="datatable-search" ng-model="data.keywords" class="form-control" placeholder="${Search}">
            <span class="input-group-btn">
              <button name="search" class="btn btn-default" type="submit"><span class="glyphicon glyphicon-search"></span></button>
            </span>
          </div>
          <button name="new" type="button" class="btn btn-primary" ng-click="clearSearch()">${Clear Search}</button>
        </form>
      </div>
      <div class="clearfix"></div>
    </div>
    <!-- body -->
    <div class="panel-body">
      <div ng-if="options.show_breadcrumbs && data.filter" class="filter-breadcrumbs">
        <sp-widget widget="data.filterBreadcrumbs"></sp-widget>
      </div>
      <div class="alert alert-info" ng-if="!data.list.length && !data.num_pages && !data.invalid_table && !loadingData">
        ${No records in {{data.table_label}} <span ng-if="data.filter">using that filter</span>}
      </div>
      <div class="alert alert-info" ng-if="loadingData">
        <fa name="spinner" spin="true"></fa> ${Loading data}...
      </div>
      <table class="table table-striped table-responsive" ng-if="data.list.length">
        <thead>
          <tr>
            <th ng-repeat="field in data.fields_array track by $index" ng-click="setOrderBy(field)">
              <div class="th-title">{{data.column_labels[field]}}</div>
              <i class="fa" ng-if="field == data.o" ng-class="{'asc': 'fa-chevron-up', 'desc': 'fa-chevron-down'}[data.d]"></i>
            </th>
          </tr>
        </thead>
        <tbody>
          <tr ng-repeat="item in data.list track by item.sys_id">
            <td class="pointer" ng-class="{selected: item.selected}" ng-click="go(data.table, item)" ng-repeat="field in data.fields_array" data-field="{{field}}" data-th="{{data.column_labels[field]}}">{{item[field].display_value}}</td>
          </tr>
        </tbody>
      </table>
      <div ng-class="{'pruned-msg-filter-pad': (!options.show_breadcrumbs || !data.filter) && !data.list.length}" class="pruned-msg" ng-if="rowsWerePruned()">
        <span ng-if="rowsPruned == 1">${{{rowsPruned}} row removed by security constraints}</span>
        <span ng-if="rowsPruned > 1">${{{rowsPruned}} rows removed by security constraints}</span>
      </div>
    </div>
    <!-- footer -->
    <div class="panel-footer" ng-hide="options.hide_footer" ng-if="data.row_count">
      <div class="btn-toolbar m-r pull-left">
        <div class="btn-group">
          <a ng-disabled="data.p == 1" href="javascript:void(0)" ng-click="setPageNum(data.p - 1)" class="btn btn-default"><i class="fa fa-chevron-left"></i></a>
        </div>
        <div ng-if="data.num_pages > 1 && data.num_pages < 20" class="btn-group">
          <a ng-repeat="i in getNumber(data.num_pages) track by $index" ng-click="setPageNum($index + 1)" href="javascript:void(0)" ng-class="{active: ($index + 1) == data.p}" type="button" class="btn btn-default">{{$index + 1}}</a>
        </div>
        <div class="btn-group">
          <a ng-disabled="data.p == data.num_pages" href="javascript:void(0)" ng-click="setPageNum(data.p + 1)" class="btn btn-default"><i class="fa fa-chevron-right"></i></a>
        </div>
      </div>
      <div class="m-t-xs panel-title">${Rows {{data.window_start + 1}} - {{ mathMin(data.window_end,data.row_count) }} of {{data.row_count}}}</div>

      <span class="clearfix"></span>
    </div>
  </div>
</div>

CSS:

.panel-heading {
  padding-left: 8px;
}

thead {
  border-bottom: 1px solid #ddd;
}

table {
  margin-bottom: 0;
}

.table > thead > tr > th {
  border: 1px solid #ddd;
  cursor: pointer;
  vertical-align: middle;

  &:first-child {
    border-left: none;
  }

  &:last-child {
    border-right: none;
  }
}

th i {
  display: inline-block;
  margin-left: 5px;
  color: #A0A0A0;
}

th .disabled{
  color:#ddd;
}

.th-title {
  display: inline-block;
  color: #428bca;
}

.panel-body {
  overflow: auto;
  padding: 0px;
}


.selected {
  color: #fff;
  background-color: #909090;
  border-color: 1px solid #fff;
}

tbody tr:last-child {
  border-bottom: none;
}

.pruned-msg {
  padding-bottom: 10px; 
  padding-left: 4px; 
  text-align: center;
}

.pruned-msg-filter-pad {
  padding-top:8px; 
}

.filter-breadcrumbs {
  border-bottom: 1px solid #ddd;
  padding-top: 3px;
}

Server:

(function() {
    if (!input) // asynch load list
        return;

    data.title = options.title || input.title;

    /*
     * data.table = the table
     * data.p = the current page starting at 1
     * data.o = the order by column
     * data.d = the order by direction
     * data.keywords = the keyword search term
     * data.list = the table data as an array
     * data.invalid_table = true if table is invalid or if data was not succesfully fetched
     * data.table_label = the table's display name. e.g. Incident
     * data.table_plural = the table's plural display name. e.g. Incidents
     * data.fields = a comma delimited list of field names to show in the data table
     * data.column_labels = a map of field name -> display name
     * data.window_size = the number of rows to show
     * data.filter = the encoded query
     * data.query_filter = encoded query template to use when searching this table
     */
    // copy to data[name] from input[name] || option[name]
    optCopy(['table', 'p', 'o', 'd', 'filter', 'filterACLs', 'fields', 'keywords', 'view']);
    optCopy(['relationship_id', 'apply_to', 'apply_to_sys_id']);
    optCopy(['query_filter']); // NYT Custom: Allow custom query
    if (!data.table) {
        data.invalid_table = true;
        data.table_label = "";
        return;
    }

    if (!data.fields) {
        if (data.view)
            data.fields = $sp.getListColumns(data.table, data.view);
        else
            data.fields = $sp.getListColumns(data.table);
    }

    data.view = data.view || 'mobile';
    data.table = data.table || $sp.getValue('table');
    data.filter = data.filter || $sp.getValue('filter');
    data.keywords = data.keywords || $sp.getValue('keywords');
    data.textSearch = data.textSearch || ($sp.getValue('text_search') == 'true');
    data.query_filter = data.query_filter || $sp.getValue('keywords') || '';
    data.p = data.p || $sp.getValue('p') || 1;
    data.p = parseInt(data.p);
    data.o = data.o || $sp.getValue('o') || $sp.getValue('order_by');
    data.d = data.d || $sp.getValue('d') || $sp.getValue('order_direction');
    data.window_size = data.window_size || $sp.getValue('maximum_entries') || 20;
    data.page_index = data.p - 1;
    data.show_new = data.show_new || options.show_new;

    /*
    | Custom data property to store if a session is encrypted
    | Used in navigation decisions to redirect or not
    |
    */
    data.isEncryptedSession = gs.isEdgeEncryptedSession();

    var gr;
    if (gs.getProperty("glide.security.ui.filter") == "true" || GlideTableDescriptor.get(data.table).getED().hasAttribute("glide.security.ui.filter")) {
        gr = new FilteredGlideRecord(data.table);
        gr.applyRowSecurity();
    } else
        gr = new GlideRecordSecure(data.table);
    if (!gr.isValid()) {
        data.invalid_table = true;
        data.table_label = data.table;
        return;
    }

    data.canCreate = gr.canCreate();
    data.newButtonUnsupported = data.table == "sys_attachment";
    data.table_label = gr.getLabel();
    data.table_plural = gr.getPlural();
    if (data.filter) {
        if (data.filterACLs)
            gr = $sp.addQueryString(gr, data.filter);
        else
            gr.addEncodedQuery(data.filter);
    }

    if (data.keywords){
        data.queryString = data.query_filter.replace(/\{q\}/g, data.keywords);
        gr.addEncodedQuery(data.queryString);
    }

    //data.filter = gr.getEncodedQuery();

    if (data.relationship_id) {
        var rel = GlideRelationship.get(data.relationship_id);
        var target = new GlideRecord(data.table);
        var applyTo = new GlideRecord(data.apply_to);
        applyTo.get("sys_id", data.apply_to_sys_id);
        rel.queryWith(applyTo, target); // put the relationship query into target
        gr.addEncodedQuery(target.getEncodedQuery()); // get the query the relationship made for us
    }

    if (data.o){
        if (data.d == "asc")
            gr.orderBy(data.o);
        else
            gr.orderByDesc(data.o);
    }

    data.window_start = data.page_index * data.window_size;
    data.window_end = (data.page_index + 1) * data.window_size;
    gr.chooseWindow(data.window_start, data.window_end);
    gr._query();

    data.row_count = gr.getRowCount();
    data.num_pages = Math.ceil(data.row_count / data.window_size);
    data.column_labels = {};
    data.fields_array = data.fields.split(',');

    // use GlideRecord to get field labels vs. GlideRecordSecure
    var grForLabels = new GlideRecord(data.table);
    for (var i in data.fields_array) {
        var field = data.fields_array[i];
        var ge = grForLabels.getElement(field);
        if (ge == null)
            continue;

        data.column_labels[field] = ge.getLabel();
    }

    data.list = [];
    while (gr._next()) {
        var record = {};
        $sp.getRecordElements(record, gr, data.fields);
        if (gr instanceof FilteredGlideRecord) {
            // FilteredGlideRecord doesn't do field-level
            // security, so take care of that here
            for (var f in data.fields_array) { 
                var fld = data.fields_array[f];
                if (!gr.isValidField(fld))
                    continue;

                if (!gr[fld].canRead()) {
                    record[fld].value = null;
                    record[fld].display_value = null;
                }
            }
        }
        record.sys_id = gr.getValue('sys_id');
        record.table = gr.getValue('sys_class_name');
        data.list.push(record);
    }

    var breadcrumbWidgetParams = { table: data.table, query: data.filter };
    data.filterBreadcrumbs = $sp.getWidget('widget-filter-breadcrumbs', breadcrumbWidgetParams);

    // copy to data from input or options
    function optCopy(names) {
        names.forEach(function(name) {
            data[name] = input[name] || options[name];
        })
    }

})();

Client:

function ($scope, $location, spUtil, amb, $http) {
    var c = this;

    /*
    * options:
    * hide_footer (bool) = true to remove the data table footer contents
    * hide_header (bool) = true to remove the data table header contents
    * show_new (bool) = true to show the "New" record button
    * show_keywords (bool) = true to show the keyword search field
    * table (string) = the table name to query
    * filter (string) = the encoded query
    * o (string) = the order by column
    * d (string) = The order by direction: asc or desc
    * p (int) = the page to jump to
    * fields (string) = comma seperated list of fields that become the list columns
    * view (string) = the default view to load for columns, overrides fields
    */

    $scope.clearSearch = function(){
        $scope.data.filter = $scope.data.storef;
        $scope.data.keywords = null;
        $scope.setSearch(true);
    };

    $scope.exportTypes = [{label:'PDF', value: 'PDF'}, {label:'Excel', value:'EXCEL'}, {label:'CSV', value:'CSV'}];
    var keys = ['table', 'filter', 'p', 'o', 'd'];

    var eventNames = {
        click: 'data_table.click',
        setFilter: 'data_table.setFilter',
        setKeywords: 'data_table.setKeywords'
    };

    $scope.go = function(table, item) {
        var parms = {};
        parms.table = item.table || table;
        parms.sys_id = item.sys_id;
        parms.record = item;
        parms.isEncryptedSession = c.data.isEncryptedSession;
        $scope.ignoreLocationChange = true;
        for (var x in c.data.list) {
            c.data.list[x].selected = false;
        }
        item.selected = true;
        $scope.$emit(eventNames.click, parms);
    };

    $scope.newRecord = function(){
        var parms = {
            id: 'form',
            table: $scope.data.table,
            sys_id: '-1'
        };
        if ($scope.data.filter != '')
            parms.query = $scope.data.filter;

        $location.search(parms);
    };

    function recoverStateFromUrl() {
        $scope.data.fields = [];
        var s = $location.search();
        for (var x in keys) {
            if (s[keys[x]]) {
                $scope.data[keys[x]] = s[keys[x]];
            }
        }
        $scope.server.update().then(function(data) {
            if (s.sys_id) {
                for (var x in data.list) {
                    if (data.list[x].sys_id == s.sys_id) {
                        $scope.go(s.table, data.list[x]);
                    }
                }
            }
        });
    }

    if ($scope.options.fromUrl) {
        $scope.$on('$locationChangeSuccess', function(e) {
            if ($scope.ignoreLocationChange){
                $scope.ignoreLocationChange = false;
                return;
            }

            // Helps to recover state when using the browser's back button
            recoverStateFromUrl();
        });
    }


    $scope.getNumber = function(num) {
        return new Array(num);
    }

    $scope.mathMin = function(v1,v2) {
        return Math.min(v1,v2);
    }

    function getData(updateUrl) {
        var f = $scope.data;
        spUtil.update($scope).then(function(data) {
            f.view = data.view;
            if ($scope.options.fromUrl && updateUrl)
                setPermalink(f.table, f.filter, f.o, f.d, f.p);

            if ($scope.options.show_breadcrumbs && data.filterBreadcrumbs)
                $scope.$broadcast('widget-filter-breadcrumbs.setBreadcrumbs', data.filterBreadcrumbs.data);

            initRecordWatcher(f.table, f.filter);
        });
    }

    function setPermalink(table, filter, orderBy, orderDirection, page){
        $scope.ignoreLocationChange = true;
        var search = $location.search();
        angular.extend(search, {
            spa: 1,
            table: table,
            filter: filter,
            p: page,
            o: orderBy,
            d: orderDirection
        });
        $location.search(search);
    }

    var watcher;
    function initRecordWatcher(table, filter){
        if (watcher)
            watcher.unsubscribe();

        if (table && filter) {
            var watcherChannel = amb.getChannelRW(table, filter);
            amb.connect();
            watcher = watcherChannel.subscribe(function() {
                spUtil.update($scope)
            });
        }
    }

    $scope.setPageNum = function(num) {
        $scope.data.p = num;
        getData(true);
    }

    $scope.setOrderBy = function(field) {
        var d = "asc";
        if ($scope.data.o == field) {
            if ($scope.data.d == "asc")
                d = "desc";
            else
                d = "asc";
        }
        $scope.data.o = field;
        $scope.data.d = d;
        $scope.setSearch(true);
    }

    $scope.setSearch = function(updateUrl) {
        $scope.data.p = 1;
        getData(updateUrl);
    }

    $scope.$on(eventNames.setFilter, function(e, newFilter){
        $scope.data.filter = newFilter;
        $scope.setSearch(false);
    });

    $scope.$on(eventNames.setKeywords, function(e, keywords){
        $scope.data.keywords = keywords;
        $scope.setSearch(false);
    });

    $scope.$on('widget-filter-breadcrumbs.queryModified', function(e, newFilter){
        $scope.data.filter = newFilter;
        $scope.setSearch(true);
    });

    $scope.rowsWerePruned = function() {
        if (!$scope.data.list)
            return;

        $scope.rowsPruned = $scope.mathMin($scope.data.window_end,$scope.data.row_count) - $scope.data.window_start - $scope.data.list.length;
        return $scope.rowsPruned > 0;
    }

    $scope.showFilter = function() {
        return !$scope.data.list.length && !$scope.data.num_pages && !$scope.data.invalid_table && !$scope.loadingData;
    }

    c.appendQuery = function(query){
        if ($scope.data.filter.length > 1)
            $scope.data.filter += '^';
        $scope.data.filter += query;
        $scope.setSearch();

    }

    // Makes Widget Async
    var title = $scope.data.title;
    if ($scope.options.use_instance_title == 'true')
        title = $scope.options.title;
    $scope.data = $scope.options;
    $scope.loadingData = true;
    $scope.server.update().then(function() {
        if ($scope.data.newButtonUnsupported)
            console.log("Service Portal: New button not supported for sys_attachment list");
        $scope.loadingData = false;
        $scope.data.title = title;
        initRecordWatcher($scope.data.table, $scope.data.filter);
    });

    function parseQuery(table, queryString){
        return $http.post('/api/now/sp/parsequery/' + table, queryString).then(function(response){
            return response.data.result;
        });
    }

    c.createQueryTerm = function(table, field, sys_id, operator){
        return $http.get('/api/now/sp/getInOutQueryTerm', {
            params: {
                table: table,
                sys_id: sys_id,
                field: field,
                operator: operator
            }
        }).then(function(response){
            if (response && response.data && response.data.result)
                return response.data.result.parts;
        });
    }

    c.showMatching = function(field, newTerm) {
        var queryString = $scope.data.filter;
        var eq = "";
        parseQuery($scope.data.table, queryString).then(function(oldTerms) {
            for(var i=0; i<oldTerms.length; i++){
                var term = oldTerms[i];
                if (isSameField(newTerm, term))
                    continue;

                if (eq.length)
                    eq += '^';

                eq += getEncodedTerm(term);
            }
            if (eq.length)
                eq += '^';
            eq += getEncodedTerm(newTerm);

            $scope.data.filter = eq;
            $scope.setSearch();
        });
    };

    c.filterOut = function(field, newTerm) {
        var eq = $scope.data.filter;
        if (eq.length)
            eq += '^';

        eq += getEncodedTerm(newTerm);
        $scope.data.filter = eq;
        $scope.setSearch();
    };

    function isSameField(t1, t2) {
        if ('left' in t1 && 'left' in t2)
            return t1.left.field === t2.left.field;
        else if ('left' in t1)
            return t1.left.field === t2.field;
        else if ('left' in t2)
            return t1.field === t2.left.field;
        return t1.field === t2.field;
    }

    function getEncodedTerm(term) {
        var eq;
        if (term.left) {
            eq = getEncodedTerm(term.left);
            eq += '^OR';
            eq += getEncodedTerm(term.right);
        } else {
            eq = term.field;
            eq += term.operator;
            eq += term.value;
        }
        return eq;
    }

}

3. Create new page called Name: My Tickets and ID: my_tickets

image

Final Result:

image

image

Bonus: (Add link in header)

got to sp_rectangle_menu_item.list and click on Newand make it same as below

image

View original source

https://www.servicenow.com/community/developer-articles/multiple-tickets-types-in-single-widget-my-tickets-page/ta-p/2303125