Comments in Service Portal My Approvals widget
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
04-09-2019 11:44 AM
One final update:
Turns out that the "track by $index" actually does introduce a bug that's only detectable if you run through several approvals consecutively without refreshing the page. The symptoms are difficult to describe verbally, but removing the track by $index fixed the problem. Putting the track by $index back in reproduced the bug with no other changes, so I'm left to conclude that it was the cause.
Updated (and Solved!):
We eventually worked out how to do this after a few "teachable moments", which I share here in hopes of sparing someone else the same frustration.
Lesson the first: When referring to examples from elsewhere, figure out for sure if they're on point. They may still have something to offer you if they're not, but if you don't realize that other person was actually working on the approvals page, where you're only dealing with one approval at a time and not the My Approvals widget while you're working on a list of them, it may take you a while to figure out that what he's doing won't translate directly to your task.
Lesson the second: Always always do a hard reload when testing a change. This is an important lesson and an easy mistake to make, and it probably cost us more time than everything else combined if we added it all up. We had probably fixed the problem several times before one of us ran it in a session where the widget wasn't cached, and we were getting the same results because we were still running the same code. And I know this because the solution that ultimately worked was one we had already tried at least once. Probably multiple times.
So what did finally get us across the finish line? I'll share that too, since there seems to be a shortage of on point answers here (that I could find anyway).
In the HTML View:
These are excerpts from different points in the HTML file, but it should be easy to figure out where they go if you clone the OOB My Approvals widget.
<!-- Added a track by $index to the ng-repeat. It may not matter, but it doesn't hurt -->
<div ng-repeat="approval in data.approvals track by $index" class="sp-approval m-b-xl">
...
<!-- Disable reject button unless the comment field is filled in.-->
<button name="reject" ng-if="approval.state == 'requested'" class="btn btn-default btn-block" ng-disabled="!approval.comments[$index]" ng-click="reject(approval.sys_id, approval.requireEsigApproval, cmt);">${Reject}</button>
<!-- Add button for deferral -->
<button name="defer" ng-if="approval.state == 'requested'" class="btn btn-default btn-block" ng-click="not_required(approval.sys_id, approval.requireEsigApproval, cmt);">${Defer}</button>
...
<div ng-if="options.portal && approval.state == 'requested'" class="col-xs-6">
<!-- Disable reject button unless the comment field is filled in.-->
<button name="reject" class="btn btn-default btn-block" ng-disabled="!approval.comments[$index]" ng-click="reject(approval.sys_id, approval.requireEsigApproval, cmt);">${Reject}</button>
</div>
<!-- Add button for deferral -->
<div ng-if="options.portal && approval.state == 'requested'" class="col-xs-6">
<button name="not_required" class="btn btn-default btn-block" ng-click="not_required(approval.sys_id, approval.requireEsigApproval, cmt);">${Defer}</button>
</div>
...
<div>
<!--Text area for comments. -->
<textarea name="comments" id="appcomments" ng-model="approval.comments[$index]" ng-change="cmt=approval.comments[$index]" style="color: grey; width: 100%; margin-top: .5em;" placeholder="Comments" class="form-control" rows="2"></textarea>
</div>
<div>
<hr style="color: grey; width: 90%; margin-top: 1em; align: center;"/>
</div>
In the Client Script:
This is mostly cloned from OOB as well, with the addition of the "cmt" argument to pass the comments back through the controller.
// Added an action for Defer, and arguments to all 3 actions to pass the "cmt" variable into the controellr, and then to the data object
$scope.approve = function(id, esigRequired, cmt){ // Add cmt as an arg
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.data.op = "approved";
$scope.data.target = id;
$scope.data.comments = cmt; // Pass comments to the data object
get();
}
}
$scope.reject = function(id, esigRequired, cmt) { // Add cmt as an arg
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.comments = cmt; // Pass comments to the data object
get();
}
}
$scope.not_required = function(id, esigRequired, cmt) { // Add cmt as an arg
var requestParams = {
username: $scope.data.esignature.username,
userSysId: $scope.data.esignature.userSysId
};
if($scope.data.esignature.e_sig_required && esigRequired) {
spUIActionsExecuter.executeFormAction(ESIGNATURE.NOT_REQUIRED_SYS, "sysapproval_approver" , id, [] , "", requestParams).then(function(response) {});
} else {
$scope.data.op = "not_required";
$scope.data.target = id;
$scope.data.comments = cmt; // Pass comments to the data object
get();
}
}
In the Server Script:
The changes here are to allow for the "defer" action, post the comment sto the approval record, and to prevent the comments from posting multiple times.
if (input) {
// update pagination
currentPage = input.pagination.currentPage;
initRow = (currentPage * maxNumberOfItemsInTheList);
lastRow = initRow + maxNumberOfItemsInTheList;
// if (input.op == 'approved' || input.op == 'rejected') {
// This prevents the comment from being posted more than once.
if (input.op == 'approved' || input.op == 'rejected' || input.op == 'not_required') { // allow for deferral
var app = new GlideRecord("sysapproval_approver");
if (app.get(input.target)) {
if (input.comments && app.state == 'requested'){
app.comments = input.comments; // Update comments if applicable
}
app.state = input.op;
app.update();
}
}
}
_______________________________________________________________________________________________
Updated:
After a good bit of thrashing around, we finally got the comments to post by binding the control to c.data.comments (which I had done before, but who knows what else changed in the meantime.)
The next problem was that the comments were posting twice -- but this was solved by tweaking the server side script thus:
// if (input.op == 'approved' || input.op == 'rejected') {
if (input.op == 'approved' || input.op == 'rejected' || input.op == 'not_required') { // allow for deferral
var app = new GlideRecord("sysapproval_approver");
if (app.get(input.target)) {
if (input.comments && app.state == 'requested'){
app.comments = input.comments; // Update comments if applicable
}
app.state = input.op;
app.update();
}
}
}
At that point all that remained was the fact that every comment box in the list was all bound to the same model (c.data.comments) so when you start typing in one of them, they all display the same value. The comment is only saved to the correct approval, but it displays in all of the fields in the list. There's a workaround in this post for that, but I'm having trouble making it work. Using the array as a model does prevent the echoing in the other textareas, but when it comes time to bring all that back to the database, it's returning "[object Object]" as the "comment" because it can't work out that it's only looking at (though to be fair, it's returning it to the correct approval record).
Thus far, no solutions for that one. But just getting a DIFFERENT error is promising at this point.
_____________________________________________________________________________________________________________
Original question:
This has to be a rookie mistake, but since we're all rookies in this shop, I'm tapping out and hoping one of the SN Jedi can point it out to me.
I've been tasked with creating a custom variant of the Approvals widget for Service Portal, with the following variances from the OOB version:
1. A "defer" option button that allows an approver to abstain from the vote. (This is working)
2. The title line should include the internally generated release number. (This is working)
Annnnd...
3. A comment field that updates the approval record so that the release manager can tell that there are comments when looking at the Related list of approvals.
This is working up to a apoint, but that point is somewhere short of actually updating the record. I can't really tell how close it comes to completing the journey, just that it never arrives.
I've seen two different solutions for this last one that are very close to what I need, tried both of them and can't get either of them to work. This one is pretty much exactly what I'm trying to do, and this one is very close to it, but I am missing some kind of connection that I am sure is so blatantly obvious I can't see it. (Because it can't be that easy, can it?)
Part of the problem is that being relatively new to SN, and (for all practical purposes) totally new to AngularJS, I opted to clone the delivered Approval widget rather than start from scratch. And both of these appear to have been built from scratch, so I'm trying to fit that logic into what I already have and there are some differences in how things are declared that may or may not be important.
I suspect that the disconnect is happening between the view and controller, since everybody seems to be in agreement about the server side code (almost verbatim in fact). I've clipped out the sections that I'm pretty sure are at the root of the problem, but I can't rule out that I missed something earlier in the HTML. But if that's the case, then the published solutions were missing it too, because the only HTML that either of them added was the <textarea> control that captured the comment at the source.
HTML Template (partial)
...
<div ng-repeat="approval in data.approvals" class="sp-approval m-b-xl">
... (right below the accept/reject/defer buttons)
<!--Text area for comments (I hope). I've tried three variations of ng-model -- with and without an id attribute -- and none works.-->
<div>
<textarea id="comments" ng-model="approval.comments" style="color: grey; width: 100%; margin-top: .5em;" placeholder="Comments" class="form-control" rows="2"></textarea>
<!--textarea id="comments" ng-model="c.data.comments" style="color: grey; width: 100%; margin-top: .5em;" placeholder="Comments" class="form-control" rows="2"></textarea-->
<!--textarea id="comments" ng-model="comments" style="color: grey; width: 100%; margin-top: .5em;" placeholder="Comments" class="form-control" rows="2"></textarea-->
</div>
...
Client script (partial)
function ($scope, $window, spUtil, spUIActionsExecuter) {
// Begin required comment for rejection logic
var c = this;
c.action = function(state, comments) {
if( (c.data.comments == undefined || c.data.comments == '' )&& state == 'rejected'){
$window.alert('Rejection Comments cannot be empty');
return false;
}
c.data.op = state;
c.data.state = state;
c.data.comments = comments; //Not optimistic this makes a difference, but worth trying
c.server.update();
};
// End required comment for rejection logic
// The rest of this is mainly OOB code for eSignature which we're not using
...
Server script (partial)
This is the one thing that seems to be universally agreed upon in all of the solutions I've seen.
The only change to OOB in this section is the addition of the defer option and the comment logic.
The if(input.comments) condition could be failing and preventing the assignment of the comments, but that means that it's not getting back to the server side script from the HTML page to begin with.
...
if (input.op == 'approved' || input.op == 'rejected' || input.op == 'not_required') { // allow for deferral
var app = new GlideRecord("sysapproval_approver");
if (app.get(input.target)) {
app.state = input.op;
// Begin custom comment logic
if (input.comments){
app.comments = input.comments; // Update comments if applicable
}
// End custom coment logic
app.update();
}
}
}
Console output (Chrome)
Widget $scope.data...
- Object
-
- ViewApprovalPageMsg: "View approval page"
- approvals: (7) [{…}, {…}, {…}, {…}, {…}, {…}, {…}]
- esignature: {e_sig_required: false, userSysId: "8e249903db346340c0ebd855ca9619d0", username: "bengelj@nccommunitycolleges.edu"}
- ids: Array(7)
-
- 0: "f78f77dedb9cbf408f75c191159619a7"
- 1: "edc5237fdbec73008f75c191159619d8"
- 2: "9ed7a37fdbec73008f75c19115961935"
- 3: "f162eb4cdbb8b3008f75c19115961933"
- 4: "ae43e78cdbb8b3008f75c19115961927"
- 5: "f8d763c0dbf8b3008f75c191159619f3"
- 6: "bde723c0dbf8b3008f75c191159619d3"
- length: 7
- __proto__: Array(0)
- myApprovals: Array(1)
-
- 0: "8e249903db346340c0ebd855ca9619d0"
- length: 1
- __proto__: Array(0)
- op: "rejected"
- pagination: {of: 11, hasPrevious: false, hasNext: false, from: 1, showPagination: false, …}
- showApprovals: true
- target: "edc5237fdbec73008f75c191159619d8"
- __proto__: Object
- 6,198 Views
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
09-22-2020 10:10 AM
I'm trying to reproduce this in my environment, with a slight change so that an approver can Comment on an approval without changing the state, rather than Deffering changing the state to not_required.
I have the button working, but i'm running into the issue that the Comment isn't being written back to the request.
I've changed the Defer button to be called Comment, and removed the op.state change to not_required. Any guesses on why my comments don't write back to the ticket?
HTML snip for Button (defer renamed to commented)
<!-- Add button for deferral -->
<button name="commented" ng-if="approval.state == 'requested'" class="btn btn-default btn-block" ng-click="commented(approval.sys_id, approval.requireEsigApproval, cmt);">${Comment}</button>
<div ng-if="options.portal && approval.state == 'requested'" class="col-xs-6">
<button name="approve" class="btn btn-primary btn-block" ng-click="approve(approval.sys_id, approval.requireEsigApproval);">${Approve}</button>
</div>
<!-- Add button for deferral -->
<div ng-if="options.portal && approval.state == 'requested'" class="col-xs-6">
<button name="commented" class="btn btn-default btn-block" ng-click="commented(approval.sys_id, approval.requireEsigApproval, cmt);">${Comment}</button>
</div>
Client Snip
$scope.commented = function(id, esigRequired, cmt) { // Add cmt as an arg
var requestParams = {
username: $scope.data.esignature.username,
userSysId: $scope.data.esignature.userSysId
};
if($scope.data.esignature.e_sig_required && esigRequired) {
spUIActionsExecuter.executeFormAction(ESIGNATURE.NOT_REQUIRED_SYS, "sysapproval_approver" , id, [] , "", requestParams).then(function(response) {});
} else {
$scope.data.target = id;
$scope.data.comments = cmt; // Pass comments to the data object
get();
}
}
Server Snip
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 == 'requested'){
var app = new GlideRecord("sysapproval_approver");
if (app.get(input.target)) {
if (input.comments && app.state == 'requested'){
app.comments = input.comments; // Update comments if applicable
}
app.update();
}
}
}
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
04-08-2021 10:10 AM
Hi, were you ever able to achieve this?
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
04-21-2021 07:33 AM
We ended up not pursuing this, so i never worked through where i had erred.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
05-12-2021 09:27 AM
Hi,
I did this by having a business rule on the approvals table which wrote the comments to the related request.
Adapted the code found in the solution here:
https://community.servicenow.com/community?id=community_question&sys_id=5b2baf771b4550107a5933f2cd4bcb3b
Hope this helps!