When it comes to updating the data from a grid, we might hit the limits like a CPU time, memory, DML.... To bypass those limitations, the updates should be executed in asynchronous mode using queueables or batches jobs.
To support this type of transactions, we provide GM - Bulk Action. With Bulk Action you get a framework to track the running tasks from a developer perspective. We also provide a monitor to visualise background tasks and let the users see the outcomes including the occurred errors.
Bulk Action Monitor Setup
To track the running bulk actions, the GM - Bulk Action Monitor should be configured on the Lightning App. Each app requiring this type of tracking should have this utility bar configured.
When the utility item is added, it should be configured as below. The width and the height should adjusted based on the label of actions, screen resolution...
Start automatically should be always checked for the monitor. If not we may loose initiated jobs before opening the monitor at the first time.
The cleanup delay defines the number of second before removing a successfully completed job. If you want your users to get a chance, put a long delay.
When a job is completed, the user get success toast message if the job is completed without any error. Otherwise he will get an error notification.
Bulk Action Implementation
Aura Action Component
Our monitor is configured, let's go ahead and start our first bulk action. Let's say that we want to move to close date by one day for the whole scope defined by the user.
This action will be triggered from an Opportunity grid. Let's create an Aura component TestBulkActionActionComponent. The component has all the required properties including:
Grid Name : Grid name coming from the configuration,
Grid Label : Grid label coming from the configuration,
Action Name : Action name coming from the configuration,
Action Label : Action label coming from the configuration,
Query : The SOQL query defining the scope
The component should also fire onsucess or oncancel based on the user flow. Finally we will use bulkActionPublisherLWC utility component to publish the job to the monitor. It's a useful component to streamline the implementation process.
<aura:componentimplements="force:hasRecordId"controller="TestBulkActionController"access="global"><!-- Action properties coming from GridMate--> <aura:attributename="relatedObjectName"type="String"access="global" /> <aura:attributename="gridLabel"type="String"access="global" /> <aura:attributename="gridName"type="String"access="global" /> <aura:attributename="gridCode"type="String"access="global" /> <aura:attributename="actionName"type="String"access="global" /> <aura:attributename="actionLabel"type="String"access="global" /><!-- Soql Query coming from GridMate--> <aura:attributename="query"type="String"access="global" /><!-- Internal flag to control the spinner--> <aura:attributename="isWorking"type="Boolean"access="global" /> <aura:registerEventname="onsuccess"type="gmpkg:DataGridActionEvent" /> <aura:registerEventname="oncancel"type="gmpkg:DataGridActionEvent" /><!-- overlayLib API --> <lightning:overlayLibraryaura:id="overlayLib" /><!-- Bulk Action Publisher --> <gmpkg:bulkActionPublisherLWCaura:id="bulkActionPublisher" /> <divclass="slds-theme_default"> <divclass="content-wrapper"> <span> {! v.query }</span> </div> <divclass="slds-modal__footer actions-wrapper"> <buttonclass="slds-button slds-button--neutral"onclick="{!c.handleCancel}">Cancel</button> <buttonclass="slds-button slds-button--brand"onclick="{!c.handleSubmit}">Submit</button> </div> <aura:ifisTrue="{! v.isWorking }"> <lightning:spinnervariant="brand"alternativeText="Processing"style="background: transparent" /> </aura:if> </div></aura:componen
({handleSubmit:function (component, event, helper) {component.set('v.isWorking',true);//Submit the action to the Salesforcelet action =component.get('c.submitBulkAction');action.setParams({ gridLabel:component.get('v.gridLabel'), gridName:component.get('v.gridName'), actionLabel:component.get('v.actionLabel'), actionName:component.get('v.actionName'), query:component.get('v.query'), url:window.location.href });action.setCallback(this,function (res) {component.set('v.isWorking',false);if (res.getState() ==='SUCCESS') {//Publish the JobId for the monitorlet bulkActionPublisher =component.find('bulkActionPublisher');bulkActionPublisher.publish(res.getReturnValue());//Show a toast for the end userlet toastEvent =$A.get('e.force:showToast'); toastEvent.setParams({ title:'Success', type:'success', message:'Action Executed Successfully' }).fire(); component.getEvent('onsuccess').setParams({ action:'ActionExecuted' }).fire();//Close the modalcomponent.find('overlayLib').notifyClose(); } elseif (res.getState() ==='ERROR') {helper.handleServerErr(res); } });$A.enqueueAction(action); },handleCancel:function (component, event, helper) { component.getEvent('oncancel').setParams({ action:'ActionCancelled' }).fire();component.find('overlayLib').notifyClose(); }});
Apex Controller Classes
Our Aura component is ready, let's go ahead and implement our Apex controller. In our example, the job of the controller is straightforward; creates a Bulk Action Job and starts an Apex batch. To get the number of records to process, we just replace the Id with Count().
The batch class track the progress of the job and keep track of the occurred errors. At the end of the batch, we close the job.
When a job is closed, the monitor is notified and the toast message is displayed to the user.
public with sharing classTestBulkActionController {@AuraEnabledpublicstaticIdsubmitBulkAction(String gridLabel,String gridName,String actionLabel,String actionName,String query,String url ) {String countQuery =query.replaceAll('Select Id ','Select count()');//Submit the job to the action managerId jobId =gmpkg.BulkActionManager.createJob(newgmpkg.BulkActionManager.BulkActionRequest( gridLabel, gridName, actionLabel, actionName, query, url,database.countQuery(countQuery) ) );//Start the batch to process all the opportunitiesDatabase.executeBatch(newTestBulkActionBatch(jobId, query));return jobId; }}
public with sharing classTestBulkActionBatchimplementsDatabase.Batchable<sObject>, Database.Stateful {publicId jobId; // Monitored jobIdpublicString scopeQuery; // Query coming from the actionpublicInteger totalProgress =0; // Current progress of the jobpublicTestBulkActionBatch(Id jobId,String scopeQuery) {this.jobId= jobId;this.scopeQuery= scopeQuery; }publicDatabase.QueryLocatorstart(Database.BatchableContext bc) {//Just replace the Id with the expected columns for the our batchreturnDatabase.getQueryLocator(this.scopeQuery.replace('Id','Id, Name, StageName, CloseDate')); }publicvoidexecute(Database.BatchableContext bc,List<Opportunity> opportunities) {if (opportunities.size() >0) {for (Opportunity opp : opportunities) {opp.CloseDate+=1; }//We prepare the list of BulkActionError to keep track of save errors List<gmpkg.BulkActionManager.BulkActionError> errorList = new List<gmpkg.BulkActionManager.BulkActionError>();
List<Database.SaveResult> resultList =database.update(opportunities,false);for (Database.SaveResult res : resultList) {if (!res.isSuccess()) { errorList.add(new gmpkg.BulkActionManager.BulkActionError(res.getId(), JSON.serialize(res.getErrors())));
} }//If we have errors, we report them back to the monitorif (errorList.size() >0) {gmpkg.BulkActionManager.reportErrors(this.jobId, errorList); }//We increase the progress and we report it back to the monitorthis.totalProgress+=opportunities.size();gmpkg.BulkActionManager.reportProgress(this.jobId,this.totalProgress); } }publicvoidfinish(Database.BatchableContext bc) {//At the end, we close the jobgmpkg.BulkActionManager.closeJob(this.jobId); }}
Bulk Action Configuration
Our Aura component and Apex Classes are ready for use. Let's go ahead and configure the grid action as below:
To trigger a Bulk Action, bulkAction flag should be set to true. If not the action will be considered as synchronous action and the query will not be passed to your component.
If confirmationRequired is set to true, the user will be asked to select the scope of the action. It could be either the selected records or the whole filter scope. See the screenshot below.