How to get a watch list in widget in Service Portal ?

Nithin12
Tera Expert

Hi Team,

How to get watch list for a particular ticket opened in service portal.

How to create a widget for this?

Any suggestions or ideas will help me a lot.

Thanks.

PlatformDeveloper Community

1 ACCEPTED SOLUTION

Hi Nithin,



Here is a quick widget I made, however it does not have any save functionality so you would have to add that. Also the sn-record-picker does not allow to add an email address as the usual watch list field does. If your goal is only to display a read of what was set on creation than this would do the job by simply setting sn-disabled="true".



HTML:


<div ng-if="data.canRead" class="panel panel-primary b">


  <div class="panel-heading">


      <h4 class="panel-title pull-left">


          ${Watch list}


      </h4>


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


  </div>


  <div class="panel-body">


          <div class="text-center text-italic text-muted">


              <sn-record-picker field="watch_list" sn-disabled="!data.canWrite" table="'sys_user'" display-field="'name'" search-fields="'name'" value-field="'sys_id'" default-query="'active=true'" multiple="true"></sn-record-picker>


          </div>


  </div>


</div>



Client script:


function($scope, spUtil) {


      var c = this;


     


      $scope.watch_list = {  


              displayValue: c.data.displayValue,  


              value: c.data.value,


              name: 'watch_list'  


  };


}



Server script:


(function() {


     


      var table = $sp.getParameter('table')


      var sys_id = $sp.getParameter('sys_id')


      var gr = new GlideRecord(table);


      if(gr.get(sys_id)){


              data.canRead = gr.watch_list.canRead();


              data.canWrite = gr.watch_list.canWrite();


              if(data.canRead){


                      data.displayValue = gr.getDisplayValue('watch_list');


                      data.value = gr.getValue('watch_list');


              }


      }


})();



I know this is incomplete but I hope it helps.


View solution in original post

65 REPLIES 65

Hi Laurent,

We have a comma in the 'name' column in the sys_user table and this is causing an issue with the record picker.  It shows two entries after selecting a user, one with the user's first name and one with their last name.  Functionally it appears to be working fine however.

Are you able suggest a fix for this?

Regards,

Robin

Hi Robin,

Sorry for the delay to answer. The sn-record-pciker is hardcoded to split on a comma so there is no way to have a comma in the display values. So you could replace the comma with something else on the server script of the widget.

Or you could create a copy of the OOB directive to support an array for the display value instead of a comma separated value. However, with this solution the live update with the record watcher would be harder to achieve as the record watcher does not use the server script and returns a comma separated value.

If you want to do this with a custom directive using a copy of the OOB here is what is required:

UI Script: This is the snRecordPicker modified to accept a displayValue array instead of a string

Name: customSnRecordPicker
Global: False
Script:

angular.module('customSnRecordPicker', [])
.directive('customSnRecordPicker', function($timeout, $http, urlTools, filterExpressionParser, $sanitize, i18n) {
    "use strict";
    var cache = {};
    function cleanLabel(val) {
        if (typeof val == "object")
            return "";
        return typeof val == "string" ? val.trim() : val;
    }
    return {
        restrict: 'E',
        replace: true,
        scope: {
            field: '=',
            table: '=',
            defaultQuery: '=?',
            startswith: '=?',
            searchFields: '=?',
            valueField: '=?',
            displayField: '=?',
            displayFields: '=?',
            pageSize: '=?',
            onChange: '&',
            snDisabled: '=',
            multiple: '=?',
            options: '=?',
            placeholder: '@'
        },
        template: '<input type="text" ng-disabled="snDisabled" style="min-width: 150px;" name="{{field.name}}" ng-model="field.value" />',
        controller: function($scope) {
            if (!angular.isNumber($scope.pageSize))
                $scope.pageSize = 20;
            if (!angular.isDefined($scope.valueField))
                $scope.valueField = 'sys_id';
            this.filterResults = function(data, page) {
                return {
                    results: data.data.result,
                    more: (page * $scope.pageSize < parseInt(data.headers('x-total-count'), 10))
                };
            }
            ;
        },
        link: function(scope, element, attrs, ctrl) {
            var isExecuting = false;
            var select2Helpers = {
                formatSelection: function(item) {
                    return $sanitize(getDisplayValue(item));
                },
                formatResult: function(item) {
                    var displayFields = getdisplayFields(item);
                    if (displayFields.length == 1)
                        return $sanitize(cleanLabel(displayFields[0]));
                    if (displayFields.length > 1) {
                        var markup = $sanitize(displayFields[0]);
                        var width = 100 / (displayFields.length - 1);
                        markup += "<div>";
                        for (var i = 1; i < displayFields.length; i++)
                            markup += "<div style='width: " + width + "%;' class='select2-additional-display-field'>" + $sanitize(cleanLabel(displayFields[i])) + "</div>";
                        markup += "</div>";
                        return markup;
                    }
                    return "";
                },
                search: function(queryParams) {
                    var url = '/api/now/table/' + scope.table + '?' + urlTools.encodeURIParameters(queryParams.data);
                    if (scope.options && scope.options.cache && cache[url])
                        return queryParams.success(cache[url]);
                    return $http.get(url).then(function(response) {
                        if (scope.options && scope.options.cache) {
                            cache[url] = response;
                        }
                        return queryParams.success(response)
                    });
                },
                initSelection: function(elem, callback) {
                    if (scope.field.displayValue) {
                        if (scope.multiple) {
                            var items = [], sel;
                            var values = scope.field.value.split(',');
                            var displayValues = scope.field.displayValue/*.split(',')*/;
                            for (var i = 0; i < values.length; i++) {
                                sel = {};
                                sel[scope.valueField] = values[i];
                                sel[scope.displayField] = displayValues[i];
                                items.push(sel);
                            }
                            callback(items);
                        } else {
                            var sel = {};
                            sel[scope.valueField] = scope.field.value;
                            sel[scope.displayField] = scope.field.displayValue;
                            callback(sel);
                        }
                    } else
                        callback([]);
                }
            };
            var config = {
                width: '100%',
                containerCssClass: 'select2-reference ng-form-element',
                placeholder: scope.placeholder || '    ',
                formatSearching: '',
                allowClear: (scope.options && typeof scope.options.allowClear !== "undefined") ? scope.options.allowClear : true,
                id: function(item) {
                    return item[scope.valueField];
                },
                ajax: {
                    quietMillis: NOW.ac_wait_time,
                    data: function(filterText, page) {
                        var params = {
                            sysparm_offset: (scope.pageSize * (page - 1)),
                            sysparm_limit: scope.pageSize,
                            sysparm_query: buildQuery(filterText, scope.searchFields, scope.defaultQuery),
                            sysparm_display_value: true
                        };
                        return params;
                    },
                    results: function(data, page) {
                        return ctrl.filterResults(data, page, scope.pageSize);
                    },
                    transport: select2Helpers.search
                },
                formatSelection: select2Helpers.formatSelection,
                formatResult: select2Helpers.formatResult,
                formatResultCssClass: function() {
                    return '';
                },
                initSelection: select2Helpers.initSelection,
                multiple: scope.multiple
            };
            function buildQuery(filterText, searchFields, defaultQuery) {
                var queryParts = [];
                var operator = "CONTAINS";
                if (scope.startswith)
                    operator = "STARTSWITH";
                if (filterText.startsWith("*")) {
                    filterText = filterText.substring(1);
                    operator = "CONTAINS";
                }
                if (defaultQuery)
                    queryParts.push(defaultQuery);
                var filterExpression = filterExpressionParser.parse(filterText, operator);
                if (searchFields != null) {
                    var fields = searchFields.split(',');
                    if (filterExpression.filterText != '') {
                        var OR = "";
                        for (var i = 0; i < fields.length; i++) {
                            queryParts.push(OR + fields[i] + filterExpression.operator + filterExpression.filterText);
                            OR = "OR";
                        }
                    }
                    for (var i = 0; i < fields.length; i++)
                        queryParts.push('ORDERBY' + fields[i]);
                    queryParts.push('EQ');
                }
                return queryParts.join('^');
            }
            scope.field = scope.field || {};
            var initTimeout = null;
            var value = scope.field.value;
            var oldValue = scope.field.value;
            var $select;
            function init() {
                element.css("opacity", 0);
                $timeout.cancel(initTimeout);
                initTimeout = $timeout(function() {
                    i18n.getMessage('Searching...', function(searchingMsg) {
                        config.formatSearching = function() {
                            return searchingMsg;
                        }
                        ;
                    });
                    element.css("opacity", 1);
                    element.select2("destroy");
                    $select = element.select2(config);
                    $select.bind("change", onChanged);
                    $select.bind("select2-selecting", onSelecting);
                    $select.bind("select2-removing", onRemoving);
                    scope.$emit('select2.ready', element);
                });
            }
            function onSelecting(e) {
                isExecuting = true;
                oldValue = scope.field.value;
                var selectedItem = e.choice;
                if (scope.multiple && selectedItem[scope.valueField] != '') {
                    var values = !scope.field.value ? [] : scope.field.value.split(',');
                    var displayValues = !scope.field.displayValue ? [] : scope.field.displayValue/*.split(',')*/;
                    values.push(selectedItem[scope.valueField]);
                    displayValues.push(getDisplayValue(selectedItem));
                    scope.field.value = values.join(',');
                    scope.field.displayValue = displayValues/*.join(',')*/;
                    e.preventDefault();
                    $select.select2('val', values).select2('close');
                    scope.$apply(function() {
                        callChange(oldValue, e);
                    });
                }
            }
            function onRemoving(e) {
                isExecuting = true;
                oldValue = scope.field.value;
                var removed = e.choice;
                if (scope.multiple) {
                    var values = scope.field.value.split(',');
                    var displayValues = scope.field.displayValue/*.split(',')*/;
                    for (var i = values.length - 1; i >= 0; i--) {
                        if (removed[scope.valueField] == values[i]) {
                            values.splice(i, 1);
                            displayValues.splice(i, 1);
                            break;
                        }
                    }
                    scope.field.value = values.join(',');
                    scope.field.displayValue = displayValues/*.join(',')*/;
                    e.preventDefault();
                    $select.select2('val', scope.field.value.split(','));
                    scope.$apply(function() {
                        callChange(oldValue, e);
                    });
                }
            }
            function callChange(oldValue, e) {
                var f = scope.field;
                var p = {
                    field: f,
                    newValue: f.value,
                    oldValue: oldValue,
                    displayValue: f.displayValue
                }
                scope.$emit("field.change", p);
                scope.$emit("field.change." + f.name, p);
                if (scope.onChange)
                    try {
                        scope.onChange(e);
                    } catch (ex) {
                        console.log("directive.customSnRecordPicker error in onChange")
                        console.log(ex)
                    }
                isExecuting = false;
            }
            function onChanged(e) {
                e.stopImmediatePropagation();
                if (scope.$$phase || scope.$root.$$phase) {
                    console.warn('in digest, returning early');
                    return;
                }
                if (e.added) {
                    var selectedItem = e.added;
                    if (!scope.multiple) {
                        scope.field.value = selectedItem[scope.valueField];
                        if (scope.field.value) {
                            scope.field.displayValue = getDisplayValue(selectedItem);
                        } else
                            scope.field.displayValue = '';
                    }
                } else if (e.removed) {
                    if (!scope.multiple) {
                        scope.field.displayValue = '';
                        scope.field.value = '';
                    }
                }
                scope.$apply(function() {
                    callChange(oldValue, e);
                });
            }
            function getDisplayValue(selectedItem) {
                var displayValue = selectedItem[scope.valueField];
                if (selectedItem) {
                    if (scope.displayField && angular.isDefined(selectedItem[scope.displayField]))
                        displayValue = selectedItem[scope.displayField];
                    else if (selectedItem.name)
                        displayValue = selectedItem.name;
                    else if (selectedItem.title)
                        displayValue = selectedItem.title;
                }
                return cleanLabel(displayValue);
            }
            function getdisplayFields(selectedItem) {
                var displayFields = [];
                if (selectedItem && selectedItem[scope.valueField]) {
                    var current = "";
                    if (scope.displayField && angular.isDefined(selectedItem[scope.displayField]))
                        current = selectedItem[scope.displayField];
                    else if (selectedItem.name)
                        current = selectedItem.name;
                    else if (selectedItem.title)
                        current = selectedItem.title;
                    displayFields.push(current);
                }
                if (scope.displayFields) {
                    var columns = scope.displayFields.split(",");
                    for (var i = 0; i < columns.length; i++) {
                        var column = columns[i];
                        if (selectedItem[column])
                            displayFields.push(selectedItem[column]);
                    }
                }
                return displayFields;
            }
            scope.$watch("field.value", function(newValue) {
                if (isExecuting)
                    return;
                if (angular.isDefined(newValue) && $select) {
                    if (scope.multiple)
                        $select.select2('val', newValue.split(',')).select2('close');
                    else
                        $select.select2('val', newValue).select2('close');
                }
            });
            if (attrs.displayValue) {
                attrs.$observe('displayValue', function(value) {
                    scope.field.value = value;
                });
            }
            init();
        }
    };
});

Widget modifications

In the widget,

  1. Replace the <sn-record-picker></sn-record-picker> tag with <custom-sn-record-picker></custom-sn-record-picker>.
  2. Remove the record watcher (unless you want to update the function to make it work with display value array).
  3. Update the display value setter in the Server script. the script should be like this
data.value = gr.getValue('watch_list') || '';
data.displayValue = data.value == '' ? [] : data.value.split(',').map(function(value){
	var user = new GlideRecord('sys_user');
	if(user.get(value)){
		return user.getDisplayValue();
	}
	return value;
});

Widget dependancy

  1. Open the widget in the platform (not widget editor).
  2. In the Dependancies related list, click New to add a new dependancy

Name: customSnRecordPicker
Include on page load: false
Angular module name: customSnRecordPicker

  1. Save
  2. In the JS includes related list, click New to add the JS Include for the custom directive

Display name: customSnRecordPicker
Source: UI Script
UI script: select the UI script you created (customSnRecordPicker)

After these steps the widget should support users with commas in their name.

Thank you Laurent, that worked perfectly.

 

Robin

Hey Laurent, I need to get the values entered in the dropdown and text input fields from a customs widget added to the Service Portal page on the server side and update the sc_request request record. Can you please help?

seandoone
Tera Contributor

Laurent,



I'm trying to add the aforementioned watch list functionality (multiple user selection on the record picker) in the Service Portal. Any idea why I'm getting an uncaught type error with the scoped attribute multiple="true"? When I remove that attribute and revert to single selection, it works. Is there something I'm missing when declaring displayValue and value in the scoped watch_list object?



Error reads: Cannot ready property of 'split' of null



Further debugging shows that it's coming from the onSelecting() function with the following line:


var values = scope.field.value == ' ' ? [] : scope.field.value.split(',');



Any help would be greatly appreciated. I'd really like to avoid a workaround for this.