Key Features
- Bulk delete
- Export selected records to CSV
- Revert delete action if record is associate with another records
Technical Highlights
- Apex Method to delete record
Demo
Code Files
Apex Controller
public with sharing class BulkDeleteController {
@AuraEnabled(cacheable=true)
public static List<sObject> getRecords(String objectName) {
String query = 'SELECT Id, Name, AccountNumber, Rating, CreatedDate FROM ' + objectName +' limit 200';
return Database.query(query);
}
@AuraEnabled(cacheable=true)
public static List<sObject> getRecordsForLstIds(List<String> recordIds, String objectName) {
try {
List<SObject> records;
if(String.isNotBlank(objectName)) {
// Handle Case Object
String fields = 'Id, Name';
if(objectName == 'Case') {
fields = 'Id, CaseNumber';
} else if(objectName == 'Task') {
fields = 'Id';
}
String query = 'SELECT ' + fields + ' FROM ' + objectName + ' WHERE Id IN :recordIds';
System.debug('query ' + query);
records= Database.query(query);
}
System.debug('records ' + records);
return records;
}
catch (Exception exc) {
return null;
}
}
@AuraEnabled
public static String deleteLstRecords(List <String> recordIds, String objectName) {
try {
Schema.DescribeSObjectResult checkObjectAccess = Schema.getGlobalDescribe().get(objectName).getDescribe();
if (checkObjectAccess.isDeletable()) {
List <Id> lstRecordsToDelete = new List <Id>();
Set <Id> deletedRecordsIds = new Set <Id>();
for (Id recordId: recordIds) {
lstRecordsToDelete.add(recordId);
}
Database.DeleteResult[] deleteResults = Database.delete(lstRecordsToDelete, true);
for (Database.DeleteResult deletedId: deleteResults) {
if (deletedId.isSuccess())
deletedRecordsIds.add(deletedId.getId());
}
}
return 'success';
} catch (Exception e) {
return 'Error deleting records: ' + e.getMessage();
}
}
}
HTML
<template>
<div>
<div class="slds-card slds-p-around_large">
<div>
<div class="slds-p-bottom_large">
<lightning-button label="Delete records" variant="destructive" onclick={openModel} disabled={deleteButtonReadOnly}></lightning-button>
<lightning-button class="slds-m-left_large" icon-name="utility:download" label="Download Records" variant="brand" onclick={downloadRecords} disabled={deleteButtonReadOnly}></lightning-button>
</div>
</div>
<div style="height: 18.75rem;">
<lightning-datatable
data={lstRecords}
columns={columns}
key-field="Id"
show-checkbox-column="true"
onrowselection={handleRecordSelection}
sorted-direction={sortDirection}
sorted-by={sortedBy}
onsort={handleSortAccountData}
onrowaction={rowActionHandler}
></lightning-datatable>
</div>
</div>
<template if:true={isModelOpen}>
<section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" class="slds-modal slds-fade-in-open slds-modal_small">
<div class="slds-modal__container slds-is-relative">
<lightning-modal-header label='Delete records'></lightning-modal-header>
<lightning-modal-body>
<div if:false={isLoaded} class="slds-is-relative spinner-margin">
<lightning-spinner alternative-text="Loading..." variant="brand"> </lightning-spinner>
</div>
<div if:true={isLoaded}>
<div if:true={havingRecordsToDelete}>
<p class="slds-align_absolute-center">Delete records</p>
</div>
<div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
<div if:true={recordToDelete}>
<lightning-layout multiple-rows>
<template for:each={recordsLst} for:item="record" for:index="index" >
<lightning-layout-item class="slds-truncate_container_60" key={record} size="3">
<lightning-pill data-record-id={record.Id} data-one="value for some" data-two="some value for two" label={record.Name} href={record.recordLink} target="_blank" onremove={removeItem} ></lightning-pill>
</lightning-layout-item>
</template>
</lightning-layout>
</div>
<div if:false={recordToDelete}>
<h1>Please select record to delete.</h1>
</div>
</div>
</div>
</lightning-modal-body>
<lightning-modal-footer>
<lightning-button variant="neutral" label="Cancel" onclick={closeModel}></lightning-button>
<lightning-button class="slds-m-left_x-small" variant="brand" label='Delete' onclick={deleteRecords} disabled={deleteDisabled}></lightning-button>
</lightning-modal-footer>
</div>
</section>
<div class="slds-backdrop slds-backdrop_open" role="presentation"></div>
</template>
</div>
</template>
Java Script
import { LightningElement, wire,track } from 'lwc';
import getRecords from '@salesforce/apex/BulkDeleteController.getRecords';
import getRecordsForLstIds from '@salesforce/apex/BulkDeleteController.getRecordsForLstIds';
import deleteLstRecords from '@salesforce/apex/BulkDeleteController.deleteLstRecords';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { NavigationMixin } from 'lightning/navigation';
export default class DatatableBulkDelete extends NavigationMixin(LightningElement) {
objectName = 'Account';
lstRecords;
selectRecordLst = [];
updatedselectRecordLst = [];
recordsLst = [];
selectedRows = [];
isModelOpen = false;
isLoaded = false;
recordToDelete = false;
deleteButtonReadOnly = true;
orgUrl;
isRemoveClicked = false;
isAcs = false;
defaultSortDirection = 'asc';
@track sortDirection = 'asc';
sortedBy = 'Name';
@track record = [];
@track recordsCount = 0;
columns = [
{ label: 'Id', fieldName: 'Id', type: 'text' },
{ label: 'Name', fieldName: 'Name', type: 'text',sortable: true },
{ label: 'Account Number', fieldName: 'AccountNumber', type: 'picklist' },
{ label: 'Rating', fieldName: 'Rating', type: 'text' },
{ label: 'Created Date', fieldName: 'CreatedDate', type: 'date' , typeAttributes:{day:'numeric',month:'short',year:'numeric', hour:'2-digit',minute:'2-digit',hour12:true}},
];
headers ={
Id:"Id",
Name:"Name",
AccountNumber:"Account Number",
Rating:"Rating",
CreatedDate:"Created Date",
}
connectedCallback() {
this.orgUrl = window.location.origin;
}
@wire(getRecords, { objectName: 'Account' })
getRecords(result) {
this.wiredRecords = result;
if (result.data) {
if(result.data.length){
this.lstRecords = result.data;
} else{
console.error('Error fetching records', result);
}
} else if (result.error) {
console.error('Error fetching records', result.error);
}
}
handleRecordSelection(event){
const selectedRows = event.detail.selectedRows;
let selectedRecords = [];
this.recordsCount = event.detail.selectedRows.length;
this.selectedRows = selectedRows
for(const row of selectedRows){
selectedRecords.push( row.Id);
}
if(selectedRecords.length) {
this.deleteButtonReadOnly = false;
this.selectRecordLst = selectedRecords.join(',');
} else {
this.deleteButtonReadOnly = true;
}
}
getRecordsForLstIds() {
let lstRecordIds = this.selectRecordLst.split(',');
getRecordsForLstIds({ recordIds: lstRecordIds, objectName: this.objectName })
.then(result => {
if (result) {
this.recordToDelete = true;
result.forEach(item => {
let keys = Object.keys(item);
let nameKey = keys.find(key => key !== 'Id');
if (nameKey) {
this.recordsLst.push({Id: item['Id'],Name: item[nameKey], recordLink: this.orgUrl + '/' + item['Id']
});
this.isLoaded = true;
}
});}
})
.catch(error => {
console.log('Error in getting ids ', error);
});
}
openModel() {
this.isModelOpen = true;
this.getRecordsForLstIds();
}
closeModel() {
this.recordsLst = [];
this.isModelOpen = false;
this.recordToDelete = false;
this.isLoaded = false;
}
deleteRecords() {
let lstRecordIds = this.selectRecordLst.split(',');
if(this.updatedselectRecordLst.length ) {
lstRecordIds = this.updatedselectRecordLst
} else if(this.isRemoveClicked && this.updatedselectRecordLst.length == 0) {
this.showtoast('No record selected');
return;
}
deleteLstRecords({ recordIds: lstRecordIds, objectName: this.objectName })
.then(result => {
if(result == 'success') {
this.showtoast(this.recordsCount + ' records deleted successfully.', 'success');
this.selectRecordLst = [];
this.recordsLst = [];
this.refreshData();
this.deleteButtonReadOnly = true;
this.isModelOpen = false;
this.recordToDelete = false;
} else {
this.deleteButtonReadOnly = true;
this.showtoast('error in deleting records : ' + result, 'error');
}
this.template.querySelector('lightning-datatable').selectedRows = [];
this.recordsCount = 0;
})
.catch(error => {
this.isModelOpen = false;
this.deleteButtonReadOnly = true;
console.log('An error occurred while deleting. ', error)
});
}
refreshData() {
refreshApex(this.wiredRecords);
}
showtoast(message, variant) {
this.dispatchEvent(
new ShowToastEvent({
title: message,
variant: variant
})
);
}
removeItem(event){
const recordId = event.target.dataset.recordId;
let updatedRecordList = this.recordsLst.filter(function(item) {
return item.Id != recordId;
});
this.recordsLst = updatedRecordList;
this.lstRecordIds = updatedRecordList;
this.updatedselectRecordLst = [];
for(let i =0 ; i< this.lstRecordIds.length; i++){
this.updatedselectRecordLst.push(this.lstRecordIds[i].Id);
}
this.isRemoveClicked = true;
}
downloadRecords(){
this.exportCSVFile(this.headers, this.selectedRows, "Record Sheet")
}
exportCSVFile(headers, totalData, fileTitle){
if(!totalData || !totalData.length){
return null
}
const jsonObject = JSON.stringify(totalData)
const result = this.convertToCSV(jsonObject, headers)
if(result === null) return
const blob = new Blob([result])
const exportedFilename = fileTitle ? fileTitle+'.csv' :'export.csv'
if(navigator.msSaveBlob){
navigator.msSaveBlob(blob, exportedFilename)
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)){
const link = window.document.createElement('a')
link.href='data:text/csv;charset=utf-8,' + encodeURI(result);
link.target="_blank"
link.download=exportedFilename
link.click()
} else {
const link = document.createElement("a")
if(link.download !== undefined){
const url = URL.createObjectURL(blob)
link.setAttribute("href", url)
link.setAttribute("download", exportedFilename)
link.style.visibility='hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
}
}
convertToCSV(objArray, headers){
const columnDelimiter = ','
const lineDelimiter = '\r\n'
const actualHeaderKey = Object.keys(headers)
const headerToShow = Object.values(headers)
let str = ''
str+=headerToShow.join(columnDelimiter)
str+=lineDelimiter
const data = typeof objArray !=='object' ? JSON.parse(objArray):objArray
data.forEach(obj=>{
let line = ''
actualHeaderKey.forEach(key=>{
if(line !=''){
line+=columnDelimiter
}
let strItem = obj[key]+''
line+=strItem? strItem.replace(/,/g, ''):strItem
})
str+=line+lineDelimiter
})
return str
}
handleSortAccountData(event) {
this.sortBy = event.detail.fieldName;
this.sortDirection = event.detail.sortDirection;
this.sortAccountData(event.detail.fieldName, event.detail.sortDirection);
}
sortAccountData(fieldname, sortDirection) {
let parseData = JSON.parse(JSON.stringify(this.lstRecords));
let keyValue = (a) => {
return a[fieldname];
};
let isReverse = sortDirection === 'asc' ? 1: -1;
parseData.sort((x, y) => {
x = keyValue(x) ? keyValue(x) : '';
y = keyValue(y) ? keyValue(y) : '';
if(x > y){
return sortDirection === 'asc' ? -1 : 1;}
else if(y > x){
return sortDirection === 'asc' ? 1 : -1;}
else return 0;
});
this.lstRecords = parseData;
}
rowActionHandler(event) {
const actionName = event.detail.action.name;
const row = event.detail.row;
switch ( actionName ) {
case 'view':
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId: row.Id,
objectApiName: 'Account',
actionName: 'view'
}
});
break;
case 'edit':
this.refreshData();
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId: row.Id,
objectApiName: 'Account',
actionName: 'edit'
}
});
break;
default:
}
}
}