back to blog
Create Editable Approval Details
November 21 2022 • 20 min read

This blog is the continuation of our previous blog Auto Approve Opportunities

BUSINESS CHALLENGE

We are expediting the approval process so that all the approval requests can be approved apart from the approval of the Opportunity Owner. But if the approval was rejected for some reason, we should be able to save info like who didn’t approve the record and the reason for rejection.

Since the standard Approval Objects don't support having custom fields, we create a custom object to store the respective approval details of each user and provide editing options.

STEPS TO ACHIEVE THE REQUIREMENT

  1. Create a Custom Object
  2. Create custom fields for the Custom Object
  3. Modify the Apex class.
  4. Create an Aura component to display and edit the custom Approval Details
  5. Add Aura Component to a new tab in the record page.

CREATE A CUSTOM OBJECT

  1. Create a Custom Object and label it as Approval Detail.

screenshot 1

  1. Enter the Record Name as Approval Process Step and let the data type be text.
  2. If reports are going to be created on this data, under Optional Features check Allow Reports option.
  3. Click Save.

CREATE CUSTOM FIELDS FOR THE CUSTOM OBJECT

  1. Click on Fields and Relationships and click New to create a new field.
  2. For each of the following fields we give access to the respective profiles as per the requirement
  3. Here we are creating 6 fields to meet the requirement.
  • Approval Status (Picklist with Approved, Pending and Rejected as values
  • Approver Id (Lookup to User)
  • Approver Name (formula field - ApproverId__r.FirstName & " " & ApproverId__r.LastName)
  • Comments (Long Text Area)
  • Date (Date/Time)
  • LinkedEntityId (Lookup to Opportunity)

MODIFY APEX CLASS

  1. Modify the previous apex class as follows
public with sharing class OpportunityAutoApproval {

    @InvocableMethod
    public static void submitAndApprove(List<Id> opportunityIds){
        System.debug('Entered submit and approve method');
        Id opportunityId = opportunityIds.get(0);
        Approval.ProcessSubmitRequest request = new Approval.ProcessSubmitRequest();
        request.setComments('Submitted for Approval');
        request.setObjectId(opportunityId);
        Approval.ProcessResult result = Approval.process(request);
        approveRecord(opportunityId);
        createApprovalHistoryRecords(opportunityId);
    }
    
    public static List<ProcessInstanceWorkitem> getWorkitems(Id targetObjectId){
        List<ProcessInstanceWorkitem> workitems = new List<ProcessInstanceWorkitem>();
        for (ProcessInstanceWorkitem pIwi : [Select p.Id, p.OriginalActorId from ProcessInstanceWorkitem p where p.ProcessInstance.TargetObjectId =: targetObjectId WITH SECURITY_ENFORCED]){
            workitems.add(pIwi);
        }
        return workitems;
    }
    
    public static void approveRecord(Id opportunityId){
        Opportunity opp = [Select Id, Name, OwnerId from Opportunity where Id = :opportunityId WITH SECURITY_ENFORCED];
        List<ProcessInstanceWorkitem> workitems = getWorkitems(opportunityId);
        Boolean pullOut = false;
        While (workitems.size() > 0 && pullOut == false){
            List<Approval.ProcessWorkitemRequest> requestList = new List<Approval.ProcessWorkitemRequest>();
            for (ProcessInstanceWorkitem piwi : workitems){
                if (piwi.OriginalActorId != opp.OwnerId){
                    Approval.ProcessWorkitemRequest request = new Approval.ProcessWorkitemRequest();
                    request.setWorkitemId(piwi.Id);
                    request.setComments('Approved the Record');
                    request.setAction('Approve');
                    requestList.add(request);     
                }else if (piwi.OriginalActorId == opp.OwnerId){
                    pullOut = true;
                    break;
                }
        	}
            if (requestList.size() > 0){
               Approval.ProcessResult[] results = Approval.process(requestList); 
            }
            if (pullOut == false) {
                workitems = getWorkitems(opportunityId);
            }
        }
    }

    public static void createApprovalHistoryRecords(Id opportunityId){
        List<ProcessInstance> piList = [Select Id, ProcessDefinitionId, CompletedDate from ProcessInstance where TargetObjectId = :opportunityId WITH SECURITY_ENFORCED ORDER BY CompletedDate DESC];
        ProcessInstance pi = piList.get(0);
        ProcessDefinition pd = [Select Id, Name from ProcessDefinition where Id = :pi.ProcessDefinitionId WITH SECURITY_ENFORCED];
        List<ProcessInstanceStep> pisList = [Select OriginalActorId, StepStatus, StepNodeId, CreatedDate from ProcessInstanceStep where ProcessInstanceId = :pi.Id AND StepStatus = 'Approved' WITH SECURITY_ENFORCED ORDER BY CreatedDate DESC];
        List<Approval_Detail__c> approvalDetailList = new List<Approval_Detail__c>();
        List<Id> processNodeIds = new List<Id>();
        List<Id> userIdList = new List<Id>();
        for (ProcessInstanceStep pis : pisList){
            userIdList.add(pis.OriginalActorId);
            processNodeIds.add(pis.StepNodeId);
        }
        Map<Id, ProcessNode> processNodeMap = new Map<Id, ProcessNode>([Select Id, Name from ProcessNode where Id = :processNodeIds]);
        Map<Id, User> userMap = new Map<Id, User>([Select Id, Name from User where Id = :userIdList]);
        for (ProcessInstanceStep piStep : pisList) {
            Approval_Detail__c approvalDetail = new Approval_Detail__c();
            approvalDetail.Date__c = piStep.CreatedDate;
            approvalDetail.Name = processNodeMap.get(piStep.StepNodeId).Name;
            if(approvalDetail.Name.contains('Bid Assessment')){
                approvalDetail.Approval_Process_Name__c = 'Bid Approval';
            }
            approvalDetail.ApproverId__c = piStep.OriginalActorId;
            approvalDetail.LinkedEntityId__c = opportunityId;
            approvalDetailList.add(approvalDetail);
        }
        if (Schema.sObjectType.Approval_Detail__c.fields.Date__c.isCreateable() && Schema.sObjectType.Approval_Detail__c.fields.Name.isCreateable() && Schema.sObjectType.Approval_Detail__c.fields.ApproverId__c.isCreateable() && Schema.sObjectType.Approval_Detail__c.fields.LinkedEntityId__c.isCreateable() && Schema.sObjectType.Approval_Detail__c.fields.Approval_Process_Name__c.isCreateable()){
            insert approvalDetailList;
        }
    }
}

CREATE AN AURA COMPONENT TO DISPLAY AND EDIT APPROVAL DETAILS

  1. Create a static Resource
  2. Create a custom Data Table Lwc component
  3. Create an Apex Controller
  4. Create an Aura Component

CREATE A STATIC RESOURCE

  1. Static resource can be created in developer console

screenshot 343

  1. Open Developer Console and click on File at the top right corner, place the cursor on New and select Static Resource.

screenshot 344

  1. Enter the Name and choose the type as css/text and click submit.

screenshot 345

Here is the code for static resource CustomDataTable.css

.slds-grid .slds-hyphenate {
    width: 100%
}

.slds-grid .slds-truncate {
    width: 100%
}

.picklist-container{
    margin-top: -1rem;
    margin-left: -0.5rem;
    position: absolute !important;
}

.picklist-container .slds-dropdown{
    position: fixed !important;
    max-height: 120px;
    max-width: fit-content;
    overflow: auto;
}

CREATE CUSTOM DATA TABLE LWC COMPONENT

  1. This CustomDataTable Lwc uses two other Lwc components in it, they are picklistTemplate and dataTablePicklist.
  2. In order to know how to deploy LWC from VS Code to the Org, check our blog Embed Lightning Component in the Navigation Bar of an App
  3. The picklistTemplate has three files

picklistTemplate.html

<template>
    <c-datatable-picklist label={typeAttributes.label} value={typeAttributes.value}
        placeholder={typeAttributes.placeholder} options={typeAttributes.options} context={typeAttributes.context}
        variant={typeAttributes.variant} name={typeAttributes.name}>
    </c-datatable-picklist>
</template>

picklistTemplate.js

import { LightningElement } from 'lwc';

export default class PicklistTemplate extends LightningElement {}

Change the isExposed tag value from false to true in the js-meta.xml file of the component before deploying to the Org, else the component won’t be exposed to use.

pickListTemplate.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
</LightningComponentBundle>
  1. Here are the dataTablePicklist files

dataTablePicklist.html

<template>
    <div class="picklistBlock" id="picklist">
        <div if:true={showPicklist} class="picklist-container">
            <lightning-combobox
                class="slds-popover slds-popover_edit slds-popover__body"
                variant={variant}
                name={name}
                label={label}
                value={value}
                placeholder={placeholder}
                options={options}
                onchange={handleChange}
                onblur={handleBlur}
                dropdown-alignment="auto">
            </lightning-combobox>
        </div>
        <div if:false={showPicklist} class="slds-table_edit_container slds-is-relative">
            <span class="slds-grid slds-grid_align-spread slds-cell-edit">
                <span class="slds-truncate" title={value}>{value}</span>
                <button data-id={context} class="slds-button slds-button_icon slds-cell-edit__button slds-m-left_x-small" tabindex="-1" title="Edit"
                    onclick={handleClick}>
                    <svg class="slds-button__icon slds-button__icon_hint slds-button__icon_lock slds-button__icon_small slds-button__icon_edit slds-icon slds-icon-text-default slds-icon_xx-small"
                        aria-hidden="true">
                        <use xlink:href="/_slds/icons/utility-sprite/svg/symbols.svg?cache=9.37.1#edit"></use>
                    </svg>
                    <span class="slds-assistive-text">Edit</span>
                </button>
            </span>
        </div>
    </div>
</template>

dataTablePicklist.js

import { LightningElement, api } from 'lwc';
import { loadStyle } from 'lightning/platformResourceLoader';
import CustomDataTableResource from '@salesforce/resourceUrl/CustomDataTable';

export default class DatatablePicklist extends LightningElement {
    @api label;
    @api placeholder;
    @api options;
    @api value;
    @api context;
    @api variant;
    @api name;
    showPicklist = false;
    picklistValueChanged = false;

    //capture the picklist change and fire a valuechange event with details payload.
    handleChange(event) {
        event.preventDefault();
        this.picklistValueChanged = true;
        this.value = event.detail.value;
        this.showPicklist = false;
        this.dispatchCustomEvent('valuechange', this.context, this.value, this.label, this.name);
    }

    //loads the custom CSS for picklist custom type on lightning datatable
    renderedCallback() {
        Promise.all([
            loadStyle(this, CustomDataTableResource),
        ]).then(() => { });
        if (!this.guid) {
            this.guid = this.template.querySelector('.picklistBlock').getAttribute('id');
            /* Register the event with this component as event payload.
            Used to identify the window click event and if click is outside the current context of picklist,
            set the dom to show the text and not the combobox */
            this.dispatchEvent(
                new CustomEvent('itemregister', {
                    bubbles: true,
                    composed: true,
                    detail: {
                        callbacks: {
                            reset: this.reset,
                        },
                        template: this.template,
                        guid: this.guid,
                        name: 'c-datatable-picklist'
                    }
                })
            );
        }
    }

    //show picklist combobox if window click is on the same context, set to text view if outside the context
    reset = (context) => {
        if (this.context !== context) {
            this.showPicklist = false;
        }
    }

    //Fire edit event on to allow to modify the picklist selection.
    handleClick(event) {
        event.preventDefault();
        event.stopPropagation();
        this.showPicklist = true;
        this.dispatchCustomEvent('edit', this.context, this.value, this.label, this.name);
    }
   
    //Blur event fired to set the combobox visibility to false.
    handleBlur(event) {
        event.preventDefault();
        this.showPicklist = false;
        if (!this.picklistValueChanged)
            this.dispatchCustomEvent('customtblur', this.context, this.value, this.label, this.name);
    }

    dispatchCustomEvent(eventName, context, value, label, name) {
        this.dispatchEvent(new CustomEvent(eventName, {

            composed: true,
            bubbles: true,
            cancelable: true,
            detail: {
                data: { context: context, value: value, label: label, name: name }
            }
        }));
    }
}

dataTablePicklist.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
</LightningComponentBundle>
  1. The html file for the customDataTable LWC should be left blank with the existing template tags.

customDataTable.html

<template>
   
</template>

customDataTable.js

import LightningDatatable from 'lightning/datatable';
import DatatablePicklistTemplate from './picklistTemplate.html';

export default class CustomDatatable extends LightningDatatable {
    static customTypes = {
        picklist: {
            template: DatatablePicklistTemplate,
            standardCellLayout: true,
            typeAttributes: ['label', 'placeholder', 'options', 'value', 'context', 'variant','name']
        }
    };
}

customDataTable.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
</LightningComponentBundle>

CREATE AN APEX CONTROLLER

Here is the Apex controller that we use in the Aura Component

public with sharing class ApprovalDetailController {
    
    @AuraEnabled(cacheable=true)
    public static List<Approval_Detail__c> pullApprovalDetails(Id opportunityId){
        return [Select Id, Name, LastModifiedDate, Approval_Status__c, Date__c, Approver_Name__c, LinkedEntityId__c, Comments__c from Approval_Detail__c where LinkedEntityId__c = :opportunityId AND Approval_Process_Name__c = 'Bid Approval' WITH SECURITY_ENFORCED ORDER BY Date__c DESC];
    }
    
    @AuraEnabled
    public static void updateApprovalDetails(List<Approval_Detail__c> approverCommentList ){
        update approverCommentList;
    }
}

CREATE AN AURA COMPONENT

  1. Go to developer console, click on File at the top right corner and move the cursor over New, select Lightning Component.

screenshot 346

  1. Name it as ApprovalDetails and click submit.
  2. The Aura component consists of three major files, Component(.cmp), controller(.js) and the helper(.js). We use the three of them in this component.
  3. Here is the component

ApprovalDetails.cmp

<aura:component controller="ApprovalDetailController" implements="flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId,lightning:availableForFlowScreens,force:lightningQuickAction">
    <aura:attribute name="data" type="Approval_Detail__c"/>
    <aura:attribute name="draftValues" type="Approval_Detail__c" default="[]"/>
    <aura:attribute name="myColumns" type="List"/>
    <aura:attribute name="errors" type="Approval_Detail__c" default="[]"/>
    <aura:attribute name="recordId" type="String" />
    <aura:handler name = "init" value = "{!this}" action = "{!c.doInIt}"/>
    
    <div style="height:400px;">
        <c:customDataTable class ="dTable" aura:id="dataTable"
                           data="{! v.data }"
                           columns="{! v.myColumns }"
                           keyField="Id"
                           errors="{! v.errors }"
                           onsave="{! c.handleSave }"
                           draftValues="{! v.draftValues }"
                           hideCheckboxColumn="true"
                           suppressBottomBar="false"
                           oncellchange="{! c.handleCellChange}"
                           onvaluechange="{! c.handleValueChange}"
                           oncancel="{! c.handleCancel}"
                           />
    </div>
</aura:component>
  1. Here is the Js file

ApprovalDetailsController.js

({
    doInIt: function (component, event, helper) {
        component.set('v.myColumns', [
            {label: 'Approval Process Step', fieldName: 'Name', type: 'text', editable: false},
            {label: 'Date', fieldName: 'LastModifiedDate', type: 'date', editable: false, typeAttributes: {  
                day: 'numeric',  
                month: 'numeric',  
                year: 'numeric',  
                hour: '2-digit',  
                minute: '2-digit',
                hour12: false}},    
            {label: 'Approver Name', fieldName: 'Approver_Name__c', type: 'text', editable: false},
            {
                label: 'Approval Status',
                fieldName: 'Approval_Status__c',
                type: 'picklist',
                editable: false,
                typeAttributes: {
                    placeholder: 'Choose Approval Status',
                    options: [
                        { label: 'Approved', value: 'Approved' },
                        { label: 'Rejected', value: 'Rejected' },
                        { label: 'Pending', value: 'Pending' }
                    ],
                    value: { fieldName: 'Approval_Status__c' },
                    context: { fieldName: 'Id' },
                    variant: 'label-hidden',
                    name: 'Approval Status',
                    label: 'Approval Status'
            	}
            },
            {label: 'Comments', fieldName: 'Comments__c', type: 'text', editable: true},
        ]);
        helper.fetchData(component, event, helper);
    },

    handleSave: function (component, event, helper) {
        var drafts = event.getParam('draftValues');
        var toastEvent = $A.get("e.force:showToast");
        toastEvent.setParams({
            title : 'Updated',
            message: 'Saved Successfully',
            duration:' 5000',
            key: 'info_alt',
            type: 'success',
            mode: 'pester'
        });
        var action = component.get("c.updateApprovalDetails");
        action.setParams({"approverCommentList" : drafts});
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS"){
            	toastEvent.fire();
                var navEvt = $A.get("e.force:navigateToSObject");
                navEvt.setParams({
                  "recordId": component.get("v.recordId")
                });
                navEvt.fire();
            }
        });
        $A.enqueueAction(action);
    },
    doRefresh: function(component, event, helper) {
    	helper.fetchDataWithoutRefresh(component, event, helper);
    },      
 	handleCellChange: function(component, event, helper) {
        let updatedDraftValues = helper.updateDraftValues(component, event.getParam('draftValues')[0]);
        component.set('v.draftValues', updatedDraftValues);
	},
    
    handleCancel: function(component, event) {
        event.preventDefault();
        var navEvt = $A.get("e.force:navigateToSObject");
        $A.get('e.force:refreshView').fire();  
    },
            
    handleValueChange: function(component, event, helper) {
        event.stopPropagation();
        let dataRecieved = event.getParam('data');
        let updatedItem;
        switch (dataRecieved.label) {
            case 'Approval Status':
                updatedItem = {
                    Id: dataRecieved.context,
                    Approval_Status__c: dataRecieved.value
                };
                break;
            default:
                this.setClassesOnData(dataRecieved.context, '', '');
                break;
        }
        let updatedDraftValues = helper.updateDraftValues(component, updatedItem);
        component.set('v.draftValues', updatedDraftValues);        
	}
})
  1. Here is the helper file

ApprovalDetailsHelper.js

({
    fetchData: function (component, event, helper) {
        var action = component.get("c.pullApprovalDetails");
        action.setParams({"opportunityId" : component.get("v.recordId")});
        action.setCallback(this, function(response) {
            var status = response.getState();
            if (status === "SUCCESS") {
                var data = response.getReturnValue();
                console.log('The data is: ' + JSON.stringify(data));
                component.set('v.data', data);
				$A.get('e.force:refreshView').fire();               
            }
        });
        $A.enqueueAction(action);
    },
    
    fetchDataWithoutRefresh: function (component, event, helper) {
        var action = component.get("c.pullApprovalDetails");
        action.setParams({"opportunityId" : component.get("v.recordId")});
        action.setCallback(this, function(response) {
            var status = response.getState();
            if (status === "SUCCESS") {
                var data = response.getReturnValue();
                console.log('The data is: ' + JSON.stringify(data));
                component.set('v.data', data);             
            }
        });
        $A.enqueueAction(action);
    },
    
    updateDraftValues: function(component, updateItem) {
        let draftValueChanged = false;
        let copyDraftValues = JSON.parse(JSON.stringify(component.get('v.draftValues')));

        copyDraftValues.forEach((item) => {
            if (item.Id === updateItem.Id) {
                for (let field in updateItem) {
                    item[field] = updateItem[field];
                }
                draftValueChanged = true;
            }
        });
        let updatedDraftValues;
        if (draftValueChanged) {
            updatedDraftValues = [...copyDraftValues];
        } else {
            updatedDraftValues = [...copyDraftValues, updateItem];
        }
        return updatedDraftValues;
    }  
})

ADD AURA COMPONENT TO A NEW TAB IN THE RECORD PAGE

  1. Go to any Opportunity Record page and click the gear icon at the top.
  2. Click on the Edit page.

screenshot 349

  1. Now select the entire Tabs component and add a new Tab called Approval Details.

screenshot 350

  1. Now drag and drop the aura component ApprovalDetails into the tab

screenshot 2

  1. Click Save at the top right corner and go back to the record page after you see the message changes saved before the save button.
  2. Now you will be able to edit the Approval Details once the record is submitted for approval.

screenshot 5 123

WRAPPING IT UP

In this blog we have covered how to create editable Approval Details for an Opportunity using LWC and Aura components.

Leave a Comment

Your email address will not be published

© 2024 Digital Biz Tech