0

HTML:

<div class="panel panel-{{::options.color}} b" ng-if="data.showApprovals">
  <div class="panel-heading">
    <h3 class="h4 panel-title">
      <fa ng-if="::options.glyph.length" name="{{::options.glyph}}" class="m-r-sm" ></fa>
      ${My Approvals}
      <label ng-if="data.pagination.showPagination && (data.pagination.from <= data.pagination.to)" class="pull-right text-info">
        <span ng-if="data.pagination.from != data.pagination.to">
          {{data.pagination.from}} ${to_lower}
        </span>
        {{data.pagination.to}} ${of} {{data.pagination.of}}
      </label>
    </h3>
  </div>
  <div class="panel-body" ng-class="{'padder-b-none': data.approvals.length != 0}">
    <div ng-if="data.approvals.length == 0 && data.pagination.of == 0" tabindex="-1" id="no-approval">
      ${You have no pending approvals}
    </div>
    <div ng-if="data.approvals.length == 0 && data.pagination.of != 0">
      ${Loading approvals...}
    </div>
    <div ng-repeat="approval in data.approvals" class="sp-approval m-b-xl">
      <div class="row">
        <div ng-class="contentColClass">
          <div ng-if="approval.task.number || approval.task.short_description">
            <a ng-href="?id=approval&table=sysapproval_approver&sys_id={{::approval.sys_id}}" id="approval_task_{{::approval.sys_id}}"title="{{data.ViewApprovalPageMsg}}">
              <span ng-if="approval.task.number">{{::approval.task.number}}</span>
              <span ng-if="approval.task.number && approval.task.short_description"> - </span>
              <span ng-if="approval.task.short_description">{{::approval.task.short_description}}</span>
            </a>
          </div>
          <div ng-if="approval.task.opened_by"><label>${Requestor}</label> {{::approval.task.opened_by}}</div>
          <div ng-if="approval.task.approver"><label>${Approver}</label> {{::approval.task.approver}}</div>
          <div ng-if="approval.task.start_date"><label>${Start}</label> {{::approval.task.start_date}}</div>
          <div ng-if="approval.task.end_date"><label>${End}</label> {{::approval.task.end_date}}</div>
          <div ng-if="approval.task.quantity"><label>${Quantity}</label> {{::approval.task.quantity}}</div>
          <div ng-if="approval.task.price"><label>${Price}</label> {{::approval.task.price}}
            <span ng-if="approval.task.recurring_price"><label>${Recurring price}</label> {{::approval.task.recurring_price}} {{::approval.task.recurring_frequency}}</span>
            <label ng-if="approval.task.quantity && approval.task.quantity > 1"> ${each}</label>
          </div>
          <div ng-if="approval.items.length == 1">
            <div ng-repeat="item in approval.items">
             <sp-widget ng-if="item.variableSummarizerWidget" widget="item.variableSummarizerWidget"></sp-widget> 
            </div>
          </div>
          <sp-widget ng-if="approval.variableSummarizerWidget" widget="approval.variableSummarizerWidget"></sp-widget>
                     </div>
        <div ng-if="!options.portal" class="col-sm-4">
          <button name="approve" id="approve_button_{{::approval.sys_id}}" aria-labelledby=" approve_button_{{::approval.sys_id}} approval_task_{{::approval.sys_id}}" ng-if="approval.state == 'requested'" class="btn btn-primary btn-block" style="border-width:1px;" ng-click="approve(approval.sys_id, approval.requireEsigApproval);">${Approve}</button>
          <button name="reject" id="reject_button_{{::approval.sys_id}}" aria-labelledby=" reject_button_{{::approval.sys_id}} approval_task_{{::approval.sys_id}}" ng-if="approval.state == 'requested'" class="btn btn-default btn-block" ng-click="reject(approval.sys_id, approval.requireEsigApproval);">${Reject}</button>
          <button ng-if="approval.state == 'approved'" class="btn btn-success btn-block">{{approval.stateLabel}}</button>
          <button ng-if="approval.state == 'rejected'" class="btn btn-danger btn-block">{{approval.stateLabel}}</button>
          <button ng-if="approval.state != 'requested'" class="btn btn-default btn-block" style="visibility:hidden">{{approval.stateLabel}}</button>
        </div>
        <div ng-if="options.portal && approval.state == 'requested'" class="col-xs-6">
          <button name="reject" ng-disabled="approvalsInProgressFor.indexOf(approval.sys_id) > -1" id="reject_button_{{::approval.sys_id}}" aria-labelledby=" reject_button_{{::approval.sys_id}} approval_task_{{::approval.sys_id}}" class="btn btn-default btn-block" ng-click="reject(approval.sys_id, approval.requireEsigApproval);">${Reject}</button>
        </div>
        <div ng-if="options.portal && approval.state == 'requested'" class="col-xs-6">
          <button name="approve" ng-disabled="approvalsInProgressFor.indexOf(approval.sys_id) > -1" id="approve_button_{{::approval.sys_id}}" aria-labelledby=" approve_button_{{::approval.sys_id}} approval_task_{{::approval.sys_id}}" class="btn btn-primary btn-block" ng-click="approve(approval.sys_id, approval.requireEsigApproval);">${Approve}</button>
        </div>
        <div ng-if="options.portal && approval.state != 'requested'" class="col-xs-12">
          <button ng-if="approval.state == 'approved'" class="btn btn-success btn-block">{{approval.stateLabel}}</button>
          <button ng-if="approval.state == 'rejected'" class="btn btn-danger btn-block">{{approval.stateLabel}}</button>
        </div>
      </div>
    </div> <!-- body -->
          </div> 
  <div class="panel-footer clearfix" ng-if="data.pagination.showPagination">
    <a id="previous-btn" href="javascript:void(0)" ng-click="previousPage()" ng-show="data.pagination.hasPrevious" class="pull-left btn btn-sm btn-default" aria-label="${Pagination button Previous}">
      <i class="fa fa-arrow-left m-r-sm" aria-hidden="true"></i>${Previous}</a>
    <a id="next_btn" href="javascript:void(0)" ng-click="nextPage()" ng-show="data.pagination.hasNext" class="pull-right btn btn-sm btn-default " aria-label="${Pagination button Next}">
      ${Next}<i class="fa fa-arrow-right m-r-sm col-md-offset-3" aria-hidden="true"></i></a>
</div>
</div>

CSS:

.pre-wrap {
  white-space:pre-wrap;
}
 .col-sm-4 {
  padding-left: 0px;
}
      @media screen and (max-width: 992px) {
  .col-sm-4 {
    padding-right: 0px;
  }
   .col-sm-8 {
    padding-left: 0px;
  }
}

Server Script:

g_approval_form_request = true;
 //we get only a max number of elements to avoid to have a big list of it
var maxNumberOfItemsInTheList = parseInt(options.max_number_of_elements_shown_on_the_list);
//set 10 if maxnumber is undefined, empty or negative value
maxNumberOfItemsInTheList = maxNumberOfItemsInTheList>0 ? maxNumberOfItemsInTheList : 10; 
var initRow = 0;
var lastRow = maxNumberOfItemsInTheList;
var currentPage = 0; //0 is the first page
if (input) {
 // update pagination
 currentPage = input.pagination.currentPage;
 initRow = (currentPage * maxNumberOfItemsInTheList);
 lastRow = initRow + maxNumberOfItemsInTheList;
   if (input.op == 'approved' || input.op == 'rejected') {
  var app = new GlideRecord("sysapproval_approver");
  if (app.get(input.target) && app.state.canWrite()) {
   app.state = input.op;
   if(input.op == 'rejected')
   {
    app.comments = input.reject_comment;
   }
   app.update();
  }
 }
}
 data.ViewApprovalPageMsg = gs.getMessage("View approval page");
data.esignature = {
 username:  gs.getUserName(),
 userSysId: gs.getUserID(),
 e_sig_required: GlidePluginManager.isRegistered('com.glide.e_signature_approvals')
};
 var esigRequiredMap = {};
if (data.esignature.e_sig_required) {
 var esigRegistryGR = new GlideRecord("e_signature_registry");
 esigRegistryGR.addQuery("enabled", "true");
 esigRegistryGR.query();
 while(esigRegistryGR.next()) {
  esigRequiredMap[esigRegistryGR.getValue("table_name")] = true;
 }
}
 var gr = new GlideRecord('sysapproval_approver');
gr.chooseWindow(initRow, lastRow);
var qc1 = gr.addQuery("state", "requested");
//if (input)
//  qc1.addOrCondition("sys_id", "IN", input.ids);
data.myApprovals = getMyApprovals();
gr.addQuery("approver", data.myApprovals);
gr.orderBy("sys_created_on");
gr.query();
var rowCount = gr.getRowCount();
var approvals = [];
var ids = [];
var source_tables = [];
 while (gr.next()) {
  var task = getRecordBeingApproved(gr);
 if (!task.isValidRecord())
  continue;
   ids.push(gr.getUniqueValue());
  var t = {};
  t.number = task.getDisplayValue();
  t.short_description = task.short_description.toString();
  if (gr.getValue("approver") != gs.getUserID())
   t.approver = gr.approver.getDisplayValue();
  if (task.isValidField("opened_by") && !task.opened_by.nil())
   t.opened_by = task.opened_by.getDisplayValue();
   // requestor >> opener
  if (task.isValidField("requested_by") && !task.requested_by.nil())
   t.opened_by = task.requested_by.getDisplayValue();
   t.start_date = task.start_date.getDisplayValue();
  t.end_date = task.end_date.getDisplayValue();
  t.quantity = task.quantity.getDisplayValue();
  t.table = task.getLabel();
  if (task.getValue("price") > 0)
   t.price = task.getDisplayValue("price");
   if (task.getValue("recurring_price") > 0)
   t.recurring_price = task.getDisplayValue("recurring_price");
   t.recurring_frequency = task.getDisplayValue("recurring_frequency");
   var items = [];
  var idx = 0;
  var itemsGR = new GlideRecord("sc_req_item");
  itemsGR.addQuery("request", task.sys_id);
  itemsGR.query();
  if (itemsGR.getRowCount() > 1)
    t.short_description = itemsGR.getRowCount() + " requested items";
   while (itemsGR.next()) {
    var item = {};
    item.short_description = itemsGR.short_description.toString();
    if (itemsGR.getValue("price") > 0)
      item.price = itemsGR.getDisplayValue("price");
    if (itemsGR.getValue("recurring_price") > 0) {
      item.recurring_price = itemsGR.getDisplayValue("recurring_price");
      item.recurring_frequency = itemsGR.getDisplayValue("recurring_frequency");
    }
    if (itemsGR.getRowCount() == 1) {
   item.variables = new GlobalServiceCatalogUtil().getVariablesForTask(itemsGR, true);
   item.variableSummarizerWidget = $sp.getWidget('sc-variable-summarizer', {'variables' : item.variables, 'toggle' : false, 'task' :t.number });
      t.short_description = itemsGR.short_description.toString();
    }
     items[idx] = item;
    idx++;
  }
   var j = {};
  j.sys_id = gr.getUniqueValue();
  j.table = gr.getRecordClassName();
 j.approval_source_table = gr.getValue("source_table");
 if (!j.approval_source_table)
  j.approval_source_table = gr.sysapproval.sys_class_name + "";
 j.requireEsigApproval = esigRequiredMap[j.approval_source_table];
 j.task = t;
  if (task) {
  j.variables = new GlobalServiceCatalogUtil().getVariablesForTask(task, true);
  j.variableSummarizerWidget = $sp.getWidget('sc-variable-summarizer', {'variables' : j.variables, 'toggle' : false, 'task': t.number });
 }
  j.items = items;
  j.state = gr.getValue("state");
  j.stateLabel = gr.state.getDisplayValue();
  approvals.push(j);
}
 data.ids = ids;
data.approvals = approvals;
data.showApprovals = gs.getUser().hasRole('approver_user');
// for pagination
data.pagination = {};
data.pagination.hasNext = (approvals.length == (parseInt(lastRow) - parseInt(initRow)) && lastRow < rowCount);
data.pagination.hasPrevious = parseInt(initRow) > 0;
data.pagination.from = parseInt(initRow + 1);
data.pagination.to = parseInt(lastRow) < parseInt(rowCount) ? parseInt(lastRow) : parseInt(rowCount);
data.pagination.of = parseInt(rowCount);
data.pagination.showPagination = data.pagination.hasPrevious || data.pagination.hasNext;
data.pagination.currentPage = data.pagination.from > data.pagination.to ? currentPage -1 : currentPage;
delete g_approval_form_request;
function getRecordBeingApproved(gr) {
  if (!gr.sysapproval.nil())
    return gr.sysapproval.getRefRecord();
   return gr.document_id.getRefRecord();
}

Client Controller

function ($scope, spUtil, spUIActionsExecuter, $timeout,spModal) {
   var ESIGNATURE = {
  TYPE: "form",
  APPROVE_SYS: "cbfe291147220100ba13a5554ee4904d",
  REJECT_SYS: "580f711147220100ba13a5554ee4904b"
 };
  if ($scope.options.portal == true || $scope.options.portal == 'true') {
  $scope.contentColClass = "col-xs-12";
  $scope.options.portal = true;
 } else {
  $scope.options.portal = false;
  $scope.contentColClass = "col-sm-8";
 }
  $scope.data.op = "";
 spUtil.recordWatch($scope, "sysapproval_approver", "state=requested^approverIN" + $scope.data.myApprovals.toString(), function(data) {
  // don't double-roundtrip if update came from record just approved/rejected
  if (data.data.sys_id != $scope.data.target)
   spUtil.update($scope);
 });
  $scope.approvalsInProgressFor = [];
 function get(id) {
  var target = $scope.data.target;
  spUtil.update($scope).then(function(result) {
   updateInProgressApprovals(target);
   $timeout(function () {
    var approvalIds = result && result.ids;
    // Set focus on last element
    if (id == -1)
     id = approvalIds[approvalIds.length - 1];
    // set focus on first element
    else if (id == 0)
     id = approvalIds[0];
    var key = id == "no-approval" ? "no-approval" : ("approval_task_" + id);
    setFocus(key);
   }, 500);
  });
 }
   function updateInProgressApprovals(id) {
  if (id)
   $scope.approvalsInProgressFor.splice($scope.approvalsInProgressFor.indexOf(id), 1);
 }
  function setFocus(id) {
  var ele = document.getElementById(id);
  if (ele)
   ele.focus();
 }
  function getNextApproval(id) {
  var approvals = $scope.data.ids,
  currentIndex = approvals.indexOf(id),
  hasNext = $scope.data.pagination.hasNext,
  hasPrevious = $scope.data.pagination.hasPrevious;
   // Action on only approval and no approval left
  if (approvals.length == 1 && !hasNext && !hasPrevious)
   return "no-approval";
  // Action on only approval in last page
  if (approvals.length == 1 && !hasNext)
   return 0;
  // Action on last approval item in current page
  if (currentIndex == approvals.length - 1 && hasNext)
   return -1;
  // Action on last approval item in last page
  if (currentIndex == approvals.length - 1)
   return approvals[currentIndex - 1];
   if (currentIndex >= 0 && currentIndex < approvals.length - 1)
   return approvals[currentIndex + 1]
 }
  $scope.approve = function(id, esigRequired) {
  var requestParams = {
   username: $scope.data.esignature.username,
   userSysId: $scope.data.esignature.userSysId
  };
   if($scope.data.esignature.e_sig_required && esigRequired) {
   spUIActionsExecuter.executeFormAction(ESIGNATURE.APPROVE_SYS, "sysapproval_approver" , id, [] , "", requestParams).then(function(response) {
   });			
  } else {
   $scope.approvalsInProgressFor.push(id);
   $scope.data.op = "approved";
   $scope.data.target = id;
   get(getNextApproval(id));
  }
  }
  $scope.reject = function(id, esigRequired) {
     spModal.open({
   title: 'Reject Approval',
   message: 'Please enter your reason for rejection, note this will be communicated with the requestor',
   input: true,
   value: $scope.name
  }).then(function(name) {
   $scope.name = name;
       var requestParams = {
    username: $scope.data.esignature.username,
    userSysId: $scope.data.esignature.userSysId
   };
       if($scope.data.esignature.e_sig_required && esigRequired) {
    spUIActionsExecuter.executeFormAction(ESIGNATURE.REJECT_SYS, "sysapproval_approver" , id, [] , "", requestParams).then(function(response) {
    });
   } else {
    $scope.data.op = "rejected";
    $scope.data.target = id;
    $scope.data.reject_comment = "Rejection reason: " + $scope.name;
    get();
   }
  });
 };
 // pagination
 $scope.previousPage = function() {
  if ($scope.data.pagination.currentPage > 1)
   $scope.data.pagination.currentPage = $scope.data.pagination.currentPage - 1;
  else
   $scope.data.pagination.currentPage = 0;
         $scope.data.op = "previous";
  $scope.data.target = null;
   get(0);
 }
  $scope.nextPage = function() {
  $scope.data.op = "next";
  $scope.data.target = null;
  $scope.data.pagination.currentPage = $scope.data.pagination.currentPage+1;
  get(0);
 }
   $scope.getItemDisplay = function(task) {
  if (task.number && task.short_description)
   return task.number + " - " + task.short_description;
     return task.number || task.short_description || "";
 }
}

ServiceNow widget with reject comments mandatory on Service Portal Working Code
Working Code Asked question July 6, 2023