Approving multiple record one by one is a painful activity. You have to open each record, click Approve & then give comment. Mass Approval lightning component will provide you a greater efficiency for this.
With this component you can see all Approval requests pending on the login User. And you can multi select and approve them on a single button click.
Component Features:
- Approvals of upto 25 objects can be seen. (Configurable)
- Multi select records and approve/reject at once.
- Ability to add comments before approving or rejecting.
- Auto query Name field to have better understanding of pending records
One important thing to notice here is that to query multiple records, query is done in FOR LOOP. But that’s not a point of worry as you can hit 100 SOQL in single transaction and it’s just 25.
Component Preview:
Step-1: Create an Apex controller and Exception Class
We need to create an Exception class first to throw the custom exception and Apex controller which will fetch all pending Approval requests of the current Login User.
Create MyException.cls Apex class.
1 2 3 4 5 |
public class MyException extends Exception { } |
Now create MassApprovalController.cls Apex class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
/* Code by CafeForce || www.cafeforce.com || support@cafeforce.com || Mandatory Header */ public class MassApprovalController { @AuraEnabled public static List<ApprovalResult> fetchApprovals() { try { List<ApprovalResult> result = new List<ApprovalResult>(); Map<Id, Id> recordAndProcessIdMap = new Map<Id, Id>(); Map<String, List<Id>> objectAndrecordIdMap = new Map<String, List<Id>>(); Id userId = UserInfo.getUserId(); for(ProcessInstanceWorkitem pIWorkItem : [SELECT Id, actorId, ProcessInstance.Status, ProcessInstance.TargetObjectId FROM ProcessInstanceWorkitem WHERE actorId =: userId LIMIT 49999]) { recordAndProcessIdMap.put(pIWorkItem.ProcessInstance.TargetObjectId, pIWorkItem.id); if(objectAndrecordIdMap.get(pIWorkItem.ProcessInstance.TargetObjectId.getSObjectType().getDescribe().getName()) == null) objectAndrecordIdMap.put(pIWorkItem.ProcessInstance.TargetObjectId.getSObjectType().getDescribe().getName(), new List<Id>()); objectAndrecordIdMap.get(pIWorkItem.ProcessInstance.TargetObjectId.getSObjectType().getDescribe().getName()).add(pIWorkItem.ProcessInstance.TargetObjectId); } Map<String, String> objectAndFieldMap = new Map<String, String>(); if(objectAndrecordIdMap.size() > 0 && objectAndrecordIdMap.size() <= 25) { for(String objName : objectAndrecordIdMap.keySet()) { String fieldName = ''; String fieldLabel = ''; Schema.DescribeSObjectResult ObjResult = Schema.getGlobalDescribe().get(objName).getDescribe(); Map<String, Schema.SObjectField> mapFields = ObjResult.fields.getMap(); for(String objField : mapFields.keySet()) { if(mapFields.get(objField).getDescribe().isNameField()) { fieldName = mapFields.get(objField).getDescribe().getName(); fieldLabel = mapFields.get(objField).getDescribe().getLabel(); objectAndFieldMap.put(objName, String.valueof(mapFields.get(objField).getDescribe().getName())); break; } } if(String.isNotBlank(fieldName)) { List<RecordData> recordList = new List<RecordData>(); List<Id> records = objectAndrecordIdMap.get(objName); String query = 'SELECT id, ' + fieldName + ' FROM ' + objName + ' WHERE id IN : records LIMIT 49999'; for(sObject rec : Database.query(query)) { recordList.add(new RecordData(rec, recordAndProcessIdMap.get(rec.Id))); } result.add(new ApprovalResult(ObjResult.getLabel(), fieldName, fieldLabel, recordList)); } } } if(recordAndProcessIdMap.size() > 0) { if(objectAndrecordIdMap.size() >= 25) throw new MyException('Approvals of more than 25 different objects found.'); else return result; } else { throw new MyException('No Pending Approvals found.'); } } catch(Exception err) { if ( String.isNotBlank( err.getMessage() ) && err.getMessage().contains( 'error:' ) ) { throw new AuraHandledException(err.getMessage().split('error:')[1].split(':')[0]); } else { throw new AuraHandledException(err.getMessage()); } } } @AuraEnabled public static void updateApprovalProcess(String processInstanceWorkitemIds, String action, String comment) { try { if(String.isNotBlank(processInstanceWorkitemIds)) { Set<String> setProcessInstanceWorkitemIds = (Set<String>) JSON.deserialize(processInstanceWorkitemIds, Set<String>.class); if(setProcessInstanceWorkitemIds != null && setProcessInstanceWorkitemIds.Size() > 0) { List<Approval.ProcessWorkitemRequest> submitReturnOrderList = new list<Approval.ProcessWorkitemRequest>(); for(String proInsWorkId : setProcessInstanceWorkitemIds) { Approval.ProcessWorkitemRequest req = new Approval.ProcessWorkitemRequest(); req.setComments(comment); req.setWorkitemId(Id.valueOf(proInsWorkId)); req.setAction(action); submitReturnOrderList.add(req); } // Submit the request for approval if(submitReturnOrderList != null && submitReturnOrderList.Size() > 0) { List<Approval.ProcessResult> resultList = Approval.process(submitReturnOrderList); } } else { throw new MyException('Invalid Data'); } } else { throw new MyException('No Pending Approvals found.'); } } catch(Exception err) { throw new AuraHandledException(err.getMessage()); } } public class ApprovalResult { @AuraEnabled public String objName; @AuraEnabled public String fieldName; @AuraEnabled public String fieldLabel; @AuraEnabled public List<RecordData> data; public ApprovalResult(String objName, String fieldName, String fieldLabel, List<RecordData> data) { this.objName = objName; this.fieldName = fieldName; this.fieldLabel = fieldLabel; this.data = data; } } public class RecordData { @AuraEnabled public sObject record; @AuraEnabled public Id processInstanceId; public RecordData(sObject record, Id processInstanceId) { this.record = record; this.processInstanceId = processInstanceId; } } } /* Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE */ |
Step-2: Now we will create the MassApproval Lightning Component.
For creating a new Lightning component, go to Developer console > New > Lightning Component. After creating the component, open the cmp file and paste the below code.
Markup for MassApproval.cmp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
<!-- Code by CafeForce || www.cafeforce.com || support@cafeforce.com || Mandatory Header --> <aura:component controller="MassApprovalController" implements="flexipage:availableForAllPageTypes"> <aura:attribute name="approvalList" type="Object"/> <c:CustomToast aura:id="notifLib"/> <!-- Events --> <aura:handler name="init" action="{!c.doInit}" value="{!this}"/> <div> <div class="multiApproval slds-p-around_large"> <lightning:spinner aura:id="Spinner" variant="brand" alternativeText="Loading"/> <div class="slds-text-heading_medium slds-p-around_x-small slds-m-bottom_medium"><b>Pending Approvals</b></div> <aura:if isTrue="{!not(empty(v.approvalList))}" > <aura:iteration items="{!v.approvalList}" var="approval"> <div class="slds-p-around_x-small" style="display: inline-flex; width: 100%;"> <div style="display: inline-flex; width: 100%;"> <lightning:icon size="small" class="headerIcon" iconName="standard:approval" alternativeText="approval" /> <p class="headerText">{!approval.objName}</p> </div> <div class="btnDiv"> <lightning:button onclick="{!c.handleReject}" disabled="{!empty(approval.data)}" name="{!approval.objName}" label="Reject" variant="brand" class="floatRight"/> <lightning:button onclick="{!c.handleApprove}" disabled="{!empty(approval.data)}" name="{!approval.objName}" label="Approve" variant="brand" class="floatRight slds-m-right_small"/> </div> </div> <div class="slds-p-around_x-small slds-m-bottom_small"> <table class="approvalTable slds-table slds-no-cell-focus slds-table_bordered slds-table_cell-buffer"> <thead> <tr class="slds-line-height_reset"> <th scope="col"> <div class="slds-truncate"></div> </th> <th scope="col"> <div class="slds-truncate">Id</div> </th> <th scope="col"> <div class="slds-truncate">{!approval.fieldLabel}</div> </th> </tr> </thead> <tbody> <aura:iteration items="{!approval.data}" var="rec"> <tr class="slds-hint-parent"> <td style="border-left: 1px solid #DDD;"> <center> <lightning:input type="checkbox" checked="{!rec.isChecked}" variant="label-hidden" /> </center> </td> <td> <a class="slds-truncate underline" href="javascript:void(0);" onclick="{!c.navigateToRecord}" id="{!rec.record.Id}" title="{!rec.record.Id}"> {!rec.record.Id} </a> </td> <td> <span class="slds-truncate" title="{!rec.record.Name}">{!rec.record.Name}</span> </td> </tr> </aura:iteration> </tbody> </table> </div> </aura:iteration> <aura:set attribute="else"> <p style="padding: 4px;">No Pending Approvals</p> </aura:set> </aura:if> </div> </div> </aura:component> <!-- Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE --> |
Notice that here we have used the CustomToast component on the 5th line. You need to include a custom toast component to show Toast Messages. You can check out the CustomToast component from HERE.
Now open the javascript file MassApprovalController.js and paste the below code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
({ doInit : function(component, event, helper) { helper.doInitHelper(component); }, handleApprove : function(component, event, helper) { helper.handleSelectedRows(component, event, 'Approve'); }, handleReject : function(component, event, helper) { helper.handleSelectedRows(component, event, 'Reject'); }, navigateToRecord: function (component, event, helper) { var recordId = event.currentTarget.id; var base_url = window.location.origin; var newUrl = base_url + '/' + recordId; window.open(newUrl, '_blank'); }, }) |
After controller, paste the below code in MassApprovalHelper.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
/* Code by CafeForce || www.cafeforce.com || support@cafeforce.com || Mandatory Header */ ({ doInitHelper : function(component) { var self = this; var action = component.get("c.fetchApprovals"); action.setCallback(this, function(response) { var result = response.getReturnValue(); console.log(result); if (response.getState() === "SUCCESS") { if(!$A.util.isEmpty(result)) { for(var i = 0; i < result.length; i++) { if(!$A.util.isEmpty(result[i].data)) { for(var j = 0; j < result[i].data.length; j++) { var nameValue = result[i].data[j].record[result[i].fieldName]; result[i].data[j].record['Name'] = nameValue; } } } component.set('v.approvalList', result); } $A.util.addClass(component.find('Spinner'), 'slds-hide'); } else { var errors = response.getError(); if (Array.isArray(errors) && errors.length && errors[0] && errors[0].message) { if(errors[0].message === 'No Pending Approvals found.') { self.showToast(component, 'Info', errors[0].message); } else { self.showToast(component, 'Error', errors[0].message); } } } }); $A.enqueueAction(action); }, handleSelectedRows : function(component, event, actionTask) { var self = this; var selectedProcesses = []; var approvalList = component.get('v.approvalList'); for(var index = 0; index < approvalList.length; index++) { if(approvalList[index].objName == event.getSource().get('v.name')) { for(var ind2 = 0; ind2 < approvalList[index].data.length; ind2++) { if(approvalList[index].data[ind2].isChecked) selectedProcesses.push(approvalList[index].data[ind2].processInstanceId); } } } if(!$A.util.isEmpty(selectedProcesses) && selectedProcesses.length > 0) { var comment = prompt("Please enter your comments. (optional)") self.updateApprovalHelper(component, selectedProcesses, comment, actionTask); } else { self.showToast(component, 'Info', 'Please select Record(s) to Approve/Reject.'); } }, updateApprovalHelper : function(component, selectedProcesses, comment, actionTask) { var self = this; $A.util.removeClass(component.find('Spinner'), 'slds-hide'); var action = component.get("c.updateApprovalProcess"); action.setParams({ 'processInstanceWorkitemIds' : JSON.stringify(selectedProcesses), 'action' : actionTask, 'comment' : comment }); action.setCallback(this, function(response) { var result = response.getReturnValue(); if (response.getState() === "SUCCESS") { component.set('v.approvalList', []); self.showToast(component, 'Success', 'Record(s) Updated Successfully'); self.doInitHelper(component); } else { var errors = response.getError(); if (Array.isArray(errors) && errors.length && errors[0] && errors[0].message) { self.showToast(component, 'Error', errors[0].message); } } }); $A.enqueueAction(action); }, showToast : function(component, type, message) { $A.util.addClass(component.find('Spinner'), 'slds-hide'); component.find('notifLib').showToastModel(message, type.toLowerCase()); } }) /* Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE */ |
Now finally paste the CSS in the MassApproval.css file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
.THIS .floatRight { float: right; } .THIS .multiApproval { background-color: #f9f7f7; } .THIS .multiApproval .btnDiv { min-width: 240px; } .THIS .multiApproval .headerIcon { height: 24px; } .THIS .multiApproval .headerText { font-size: 1rem; line-height: 1.25; font-weight: 700; padding: 3px; } .THIS .approvalTable thead th { background-color:#ecebeb !important; } .THIS .approvalTable thead tr th:nth-child(1){ width:15%; } .THIS .approvalTable thead tr th:nth-child(2){ width:35%; } .THIS .approvalTable thead tr th:nth-child(3){ width:50%; } |
Step-3: Now we will call the component.
1 2 3 4 5 6 |
<!-- Code by CafeForce || www.cafeforce.com || support@cafeforce.com || Mandatory Header --> <aura:application extends="force:slds"> <c:MassApproval></c:MassApproval> </aura:application> |
Finally, our component is ready to use. You can call this component in any other Lightning component or Lightning Application. Besides, you can place this component on Home Detail page or Record Detail page from Lightning App Builder. All Set Now. Approve as many request as you want.
Ability to add Approval Comments.
Also Check:
For any queries or suggestions, comment below.
Cheers … Happy Coding … 🙂
How we can edit based on Case Object
Hi Prem,
Approval on the Case object will be automatically visible in this component.
Am getting component error when i try to add in the Lightning home page.
Unable to find action ‘fetchApprovals’ on the controller of c:MassApproval
Hi Vivekh
Please check if you created the Apex class correctly with the same method name and ensure you added @AuraEnabled before the method.