Skip to content

Commit

Permalink
Added 'mass delete' button on Log__c list views (#123)
Browse files Browse the repository at this point in the history
* Added MassDelete button on Log__c list views to display VF page LogMassDelete

* New extension class LogMassDeleteExtension handles the deletion logic. It queries UserRecordAccess to make sure that only Log__c records that the user has permission to delete will be included - any other records selected in the list view are ignored

* Added details to README about new 'mass delete' button
  • Loading branch information
jongpie authored Mar 26, 2021
1 parent 9344a3c commit 56f1a9e
Show file tree
Hide file tree
Showing 16 changed files with 277 additions and 5 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,29 @@ To help development and support teams better manage logs (and any underlying cod
* `Log__c.IsResolved__c` - Indicates if the log is resolved (meaning that it required analaysis/work, which has been completed). Only closed statuses can be considered resolved. This is also driven based on the selected status (and associated config in the 'Log Status' custom metadata type)
* To customize the statuses provided, simply update the picklist values for `Log__c.Status__c` and create/update corresponding records in the custom metadata type `LogStatus__mdt`. This custom metadata type controls which statuses are considerd closed and resolved.

## View Related Log Entries
## View Related Log Entries on a Record Page
Within App Builder, admins can add the 'Related Log Entries' lightning web component to any record page. Admins can also control which columns are displayed be creating & selecting a field set on `LogEntry__c` with the desired fields.
* The component automatically shows any related log entries, based on `LogEntry__c.RecordId__c == :recordId`
* Users can search the list of log entries for a particular record using the component's built-insearch box. The component dynamically searches all related log entries using SOSL.
* Component automatically enforces Salesforce's security model
* Object-Level Security - Users without read access to LogEntry__c will not see the component
* Object-Level Security - Users without read access to `LogEntry__c` will not see the component
* Record-Level Security - Users will only see records that have been shared with them
* Field-Level Security - Users will only see the fields within the field set that they have access to

![Related Log Entries](./content/relate-log-entries-lwc.png)
## Deleting Old Logs
Admins can easily delete old logs using 2 methods: list views or Apex batch jobs
### Mass Deleting with List Views
Salesforce (still) does not support mass deleting records out-of-the-box. There's been [an Idea for 11+ years](https://trailblazer.salesforce.com/ideaView?id=08730000000BqczAAC) about it, but it's still not standard functionality. A custom button is available on `Log__c` list views to provide mass deletion functionality.
1. Users can select 1 or more `Log__c` records from the list view to choose which logs will be deleted

![Mass Delete Selection](./content/log-mass-delete-selection.png)

2. The button shows a Visualforce page `LogMassDelete` to confirm that the user wants to delete the records

![Mass Delete Confirmation](./content/log-mass-delete-confirmation.png)

### Batch Deleting with Apex Jobs
Two Apex classes are provided out-of-the-box to handle automatically deleting old logs
1. `LogBatchPurger` - this batch Apex class will delete any `Log__c` records with `Log__c.LogRetentionDate__c <= System.today()`.
* By default, this field is populated with "TODAY + 14 DAYS" - the number of days to retain a log can be customized in `LoggerSettings__c`.
Expand Down
Binary file added content/log-mass-delete-confirmation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added content/log-mass-delete-selection.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//------------------------------------------------------------------------------------------------//
// This file is part of the Nebula Logger project, released under the MIT License. //
// See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. //
//------------------------------------------------------------------------------------------------//

/**
* @group log-management
* @description Manages mass deleting `Log__c` records that have been selected by a user on a `Log__c` list view
*/
public with sharing class LogMassDeleteExtension {
private ApexPages.StandardSetController controller;

/**
* @description LogMassDeleteExtension description
* @param controller controller description
* @return return description
*/
public LogMassDeleteExtension(ApexPages.StandardSetController controller) {
if (Schema.Log__c.SObjectType.getDescribe().isDeletable() == false) {
String deleteAccessError = 'You do not have access to delete logs records';
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, deleteAccessError));
}

this.controller = controller;
}

/**
* @description Filters the list of selected `Log__c` records to only include records that the current user can delete (based on object-level access)
* @return return The matching `Log__c` records that the current user has access to delete
*/
public List<Log__c> getDeletableLogs() {
// The UserRecordAccess object is weird - RecordId is not an actual ID field, so you can't filter using `List<SObject>` or `List<Id>`, you have to use strings
// So, here's some code that would be unnecessary if RecordId were a polymorphic ID field instead
List<String> logIds = new List<String>();
for (Log__c selectedLog : (List<Log__c>) this.controller.getSelected()) {
logIds.add(selectedLog.Id);
}

// Get the list of record IDs that the current user can delete
List<Id> deletableLogIds = new List<Id>();
for (UserRecordAccess recordAccess : [
SELECT RecordId
FROM UserRecordAccess
WHERE UserId = :UserInfo.getUserId() AND RecordId IN :logIds AND HasDeleteAccess = TRUE
]) {
deletableLogIds.add(recordAccess.RecordId);
}

// Get the logs + any fields shown in the VF page
return [SELECT Id, Name, LoggedBy__c, LoggedBy__r.Name, StartTime__c, TotalLogEntries__c FROM Log__c WHERE Id IN :deletableLogIds];
}

public PageReference deleteSelectedLogs() {
try {
delete getDeletableLogs();
} catch (Exception ex) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, ex.getMessage()));
}

// The controller's method cancel() just returns the user to the previous page - it doesn't rollback any DML statements (like the delete above)
return this.controller.cancel();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,10 @@
<customTabListAdditionalFields>TotalERRORLogEntries__c</customTabListAdditionalFields>
<customTabListAdditionalFields>TotalWARNLogEntries__c</customTabListAdditionalFields>
<excludedStandardButtons>New</excludedStandardButtons>
<excludedStandardButtons>PrintableListView</excludedStandardButtons>
<excludedStandardButtons>Accept</excludedStandardButtons>
<excludedStandardButtons>OpenListInQuip</excludedStandardButtons>
<excludedStandardButtons>Accept</excludedStandardButtons>
<excludedStandardButtons>PrintableListView</excludedStandardButtons>
<listViewButtons>MassDelete</listViewButtons>
<lookupDialogsAdditionalFields>LoggedByUsernameLink__c</lookupDialogsAdditionalFields>
<lookupDialogsAdditionalFields>StartTime__c</lookupDialogsAdditionalFields>
<lookupDialogsAdditionalFields>OWNER.ALIAS</lookupDialogsAdditionalFields>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<WebLink xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>MassDelete</fullName>
<availability>online</availability>
<displayType>massActionButton</displayType>
<height>600</height>
<linkType>page</linkType>
<masterLabel>Mass Delete</masterLabel>
<openType>sidebar</openType>
<page>LogMassDelete</page>
<protected>false</protected>
<requireRowSelection>true</requireRowSelection>
</WebLink>
64 changes: 64 additions & 0 deletions nebula-logger/main/log-management/pages/LogMassDelete.page
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!------------------------------------------------------------------------------------------------//
// This file is part of the Nebula Logger project, released under the MIT License. //
// See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. //
//---------------------------------------------------------------------------------------------- -->

<apex:page standardController="Log__c" recordSetVar="logs" extensions="LogMassDeleteExtension" tabStyle="Log__c" lightningStyleSheets="true">
<apex:slds />

<script>
// TODO revisit how toast message is shown - it needs to be triggered based on success/failure result of Apex method
function showSuccessMessage() {
setTimeout(
sforce.one.showToast({
type : 'Success',
title : 'Logs Successfully Deleted',
message : '{!deletableLogs.size} record(s) were deleted'
}),
2000
);
}
</script>

<div class="wrapper" style="height:100%">
<section role="alertdialog" tabindex="0" aria-labelledby="prompt-heading-id" aria-describedby="prompt-message-wrapper" class="slds-modal slds-fade-in-open slds-modal_prompt" aria-modal="true">
<div class="slds-modal__container">
<header class="slds-modal__header slds-theme_error slds-theme_alert-texture">
<h2 class="slds-text-heading_medium" id="prompt-heading-id">Delete {!deletableLogs.size} Logs</h2>
</header>
<div class="slds-modal__content slds-p-around_medium" id="prompt-message-wrapper">
<div class="slds-p-vertical_medium">
Are you sure that you want to delete these logs?
</div>
<table class="slds-table slds-table_cell-buffer slds-table_bordered slds-table_striped">
<thead>
<tr>
<th scope="col">{!$ObjectType.Log__c.Fields.Name.Label}</th>
<th scope="col">{!$ObjectType.Log__c.Fields.LoggedBy__c.Label}</th>
<th scope="col">{!$ObjectType.Log__c.Fields.StartTime__c.Label}</th>
<th scope="col">{!$ObjectType.Log__c.Fields.TotalLogEntries__c.Label}</th>
</tr>
</thead>
<tbody>
<apex:repeat value="{!deletableLogs}" var="deletableLog">
<tr>
<td><a href="{! '/' + deletableLog.Id}">{!deletableLog.Name}</a></td>
<td><a href="{! '/' + deletableLog.LoggedBy__c}">{!deletableLog.LoggedBy__r.Name}</a></td>
<td><apex:outputField value="{!deletableLog.StartTime__c}"/></td>
<td>{!deletableLog.TotalLogEntries__c}</td>
</tr>
</apex:repeat>
</tbody>
</table>
</div>
<footer class="slds-modal__footer slds-theme_default">
<apex:form>
<apex:commandButton action="{!cancel}" value="Cancel" styleClass="slds-button slds-button_neutral" />
<apex:commandButton action="{!deleteSelectedLogs}" onclick="showSuccessMessage();" value="Delete Logs" styleClass="slds-button slds-button_destructive" />
</apex:form>
</footer>
</div>
</section>
<div class="slds-backdrop slds-backdrop_open" />
</div>
</apex:page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexPage xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<availableInTouch>false</availableInTouch>
<confirmationTokenRequired>false</confirmationTokenRequired>
<label>LogMassDelete</label>
</ApexPage>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
<apexClass>LogEntryEventBuilder</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>LogMassDeleteExtension</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>LogMessage</apexClass>
<enabled>true</enabled>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<apexClass>FlowRecordLogEntry</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>LogMassDeleteExtension</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>LogMessage</apexClass>
<enabled>true</enabled>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<application>LoggerConsole</application>
<visible>true</visible>
</applicationVisibilities>
<classAccesses>
<apexClass>LogMassDeleteExtension</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>RelatedLogEntriesController</apexClass>
<enabled>true</enabled>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@
<apexClass>LogHandler_Tests</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>LogMassDeleteExtension</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>LogMessage</apexClass>
<enabled>true</enabled>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<QuickAction xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Manage Log</label>
<label>Manage</label>
<optionsCreateFeedItem>false</optionsCreateFeedItem>
<quickActionLayout>
<layoutSectionStyle>TwoColumnsLeftToRight</layoutSectionStyle>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//------------------------------------------------------------------------------------------------//
// This file is part of the Nebula Logger project, released under the MIT License. //
// See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. //
//------------------------------------------------------------------------------------------------//

@isTest
private class LogMassDeleteExtension_Tests {

@TestSetup
static void setupData() {
List<Log__c> logs = new List<Log__c>();
for (Integer i = 0; i < 10; i++) {
Log__c log = new Log__c(TransactionId__c = 'TXN-' + i);
logs.add(log);
}
insert logs;
}

@isTest
static void it_should_return_deletable_logs() {
List<Log__c> logs = [SELECT Id, Name FROM Log__c];

List<String> logIds = new List<String>();
for (Log__c selectedLog : logs) {
logIds.add(selectedLog.Id);
}

List<Log__c> expectedDeletableLogs = new List<Log__c>();
for (UserRecordAccess recordAccess : [SELECT RecordId FROM UserRecordAccess WHERE UserId = :UserInfo.getUserId() AND RecordId IN :logIds AND HasDeleteAccess = true]) {
expectedDeletableLogs.add(new Log__c(Id = recordAccess.RecordId));
}

ApexPages.StandardSetController controller = new ApexPages.StandardSetController(logs);
controller.setSelected(logs);

PageReference pageReference = Page.LogMassDelete;
Test.setCurrentPage(pageReference);

Test.startTest();

LogMassDeleteExtension extension = new LogMassDeleteExtension(controller);
List<Log__c> returnedDeletableLogs = extension.getDeletableLogs();

Test.stopTest();

System.assertEquals(expectedDeletableLogs.size(), returnedDeletableLogs.size());
}

@isTest
static void it_should_delete_selected_log_records() {
List<Log__c> logs = [SELECT Id, Name FROM Log__c];
List<Log__c> logsToDelete = new List<Log__c>();
List<Log__c> logsToKeep = new List<Log__c>();
Integer numberToKeep = 3;
for (Integer i = 0; i < logs.size(); i++) {
if (i < numberToKeep) {
logsToDelete.add(logs.get(i));
} else {
logsToKeep.add(logs.get(i));
}
}

ApexPages.StandardSetController controller = new ApexPages.StandardSetController(logs);
controller.setSelected(logsToDelete);

PageReference pageReference = Page.LogMassDelete;
Test.setCurrentPage(pageReference);

Test.startTest();

LogMassDeleteExtension extension = new LogMassDeleteExtension(controller);
extension.deleteSelectedLogs();

Test.stopTest();

// Verify that only the selected logs were deleted
logsToDelete = [SELECT Id, IsDeleted FROM Log__c WHERE Id IN :logsToDelete ALL ROWS];
for (Log__c log : logsToDelete) {
System.assertEquals(true, log.IsDeleted, log);
}
logsToKeep = [SELECT Id, IsDeleted FROM Log__c WHERE Id IN :logsToKeep ALL ROWS];
for (Log__c log : logsToKeep) {
System.assertEquals(false, log.IsDeleted, log);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<status>Active</status>
</ApexClass>

0 comments on commit 56f1a9e

Please sign in to comment.