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