diff --git a/README.md b/README.md index c488a06c8..ae50e9006 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/content/log-mass-delete-confirmation.png b/content/log-mass-delete-confirmation.png new file mode 100644 index 000000000..296a5bebb Binary files /dev/null and b/content/log-mass-delete-confirmation.png differ diff --git a/content/log-mass-delete-selection.png b/content/log-mass-delete-selection.png new file mode 100644 index 000000000..074c2f0e0 Binary files /dev/null and b/content/log-mass-delete-selection.png differ diff --git a/nebula-logger/main/log-management/classes/LogMassDeleteExtension.cls b/nebula-logger/main/log-management/classes/LogMassDeleteExtension.cls new file mode 100644 index 000000000..4a9683ab9 --- /dev/null +++ b/nebula-logger/main/log-management/classes/LogMassDeleteExtension.cls @@ -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 getDeletableLogs() { + // The UserRecordAccess object is weird - RecordId is not an actual ID field, so you can't filter using `List` or `List`, you have to use strings + // So, here's some code that would be unnecessary if RecordId were a polymorphic ID field instead + List logIds = new List(); + for (Log__c selectedLog : (List) this.controller.getSelected()) { + logIds.add(selectedLog.Id); + } + + // Get the list of record IDs that the current user can delete + List deletableLogIds = new List(); + 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(); + } +} diff --git a/nebula-logger/main/log-management/classes/LogMassDeleteExtension.cls-meta.xml b/nebula-logger/main/log-management/classes/LogMassDeleteExtension.cls-meta.xml new file mode 100644 index 000000000..d75b0582f --- /dev/null +++ b/nebula-logger/main/log-management/classes/LogMassDeleteExtension.cls-meta.xml @@ -0,0 +1,5 @@ + + + 51.0 + Active + diff --git a/nebula-logger/main/log-management/objects/Log__c/Log__c.object-meta.xml b/nebula-logger/main/log-management/objects/Log__c/Log__c.object-meta.xml index afe5a8323..5e19e6a1d 100644 --- a/nebula-logger/main/log-management/objects/Log__c/Log__c.object-meta.xml +++ b/nebula-logger/main/log-management/objects/Log__c/Log__c.object-meta.xml @@ -179,9 +179,10 @@ TotalERRORLogEntries__c TotalWARNLogEntries__c New - PrintableListView - Accept OpenListInQuip + Accept + PrintableListView + MassDelete LoggedByUsernameLink__c StartTime__c OWNER.ALIAS diff --git a/nebula-logger/main/log-management/objects/Log__c/webLinks/MassDelete.webLink-meta.xml b/nebula-logger/main/log-management/objects/Log__c/webLinks/MassDelete.webLink-meta.xml new file mode 100644 index 000000000..32c5c873f --- /dev/null +++ b/nebula-logger/main/log-management/objects/Log__c/webLinks/MassDelete.webLink-meta.xml @@ -0,0 +1,13 @@ + + + MassDelete + online + massActionButton + 600 + page + Mass Delete + sidebar + LogMassDelete + false + true + diff --git a/nebula-logger/main/log-management/pages/LogMassDelete.page b/nebula-logger/main/log-management/pages/LogMassDelete.page new file mode 100644 index 000000000..782789aa8 --- /dev/null +++ b/nebula-logger/main/log-management/pages/LogMassDelete.page @@ -0,0 +1,64 @@ + + + + + + + +
+
+
+
+

Delete {!deletableLogs.size} Logs

+
+
+
+ Are you sure that you want to delete these logs? +
+ + + + + + + + + + + + + + + + + + + +
{!$ObjectType.Log__c.Fields.Name.Label}{!$ObjectType.Log__c.Fields.LoggedBy__c.Label}{!$ObjectType.Log__c.Fields.StartTime__c.Label}{!$ObjectType.Log__c.Fields.TotalLogEntries__c.Label}
{!deletableLog.Name}{!deletableLog.LoggedBy__r.Name}{!deletableLog.TotalLogEntries__c}
+
+
+ + + + +
+
+
+
+
+ \ No newline at end of file diff --git a/nebula-logger/main/log-management/pages/LogMassDelete.page-meta.xml b/nebula-logger/main/log-management/pages/LogMassDelete.page-meta.xml new file mode 100644 index 000000000..81cbc2b4a --- /dev/null +++ b/nebula-logger/main/log-management/pages/LogMassDelete.page-meta.xml @@ -0,0 +1,7 @@ + + + 51.0 + false + false + + diff --git a/nebula-logger/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml b/nebula-logger/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml index 918b4e009..8128f596e 100644 --- a/nebula-logger/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml +++ b/nebula-logger/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml @@ -24,6 +24,10 @@ LogEntryEventBuilder true + + LogMassDeleteExtension + true + LogMessage true diff --git a/nebula-logger/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml b/nebula-logger/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml index 1660f981c..f054a06cd 100644 --- a/nebula-logger/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml +++ b/nebula-logger/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml @@ -8,6 +8,10 @@ FlowRecordLogEntry true + + LogMassDeleteExtension + true + LogMessage true diff --git a/nebula-logger/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml b/nebula-logger/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml index cfbd4098d..b41941054 100644 --- a/nebula-logger/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml +++ b/nebula-logger/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml @@ -4,6 +4,10 @@ LoggerConsole true + + LogMassDeleteExtension + true + RelatedLogEntriesController true diff --git a/nebula-logger/main/log-management/profiles/Admin.profile-meta.xml b/nebula-logger/main/log-management/profiles/Admin.profile-meta.xml index a4ba4b997..7dbf87e5e 100644 --- a/nebula-logger/main/log-management/profiles/Admin.profile-meta.xml +++ b/nebula-logger/main/log-management/profiles/Admin.profile-meta.xml @@ -77,6 +77,10 @@ LogHandler_Tests true + + LogMassDeleteExtension + true + LogMessage true diff --git a/nebula-logger/main/log-management/quickActions/Log__c.Manage.quickAction-meta.xml b/nebula-logger/main/log-management/quickActions/Log__c.Manage.quickAction-meta.xml index f682b97ed..e49c137bf 100644 --- a/nebula-logger/main/log-management/quickActions/Log__c.Manage.quickAction-meta.xml +++ b/nebula-logger/main/log-management/quickActions/Log__c.Manage.quickAction-meta.xml @@ -1,6 +1,6 @@ - + false TwoColumnsLeftToRight diff --git a/nebula-logger/tests/log-management/classes/LogMassDeleteExtension_Tests.cls b/nebula-logger/tests/log-management/classes/LogMassDeleteExtension_Tests.cls new file mode 100644 index 000000000..4af9b1f78 --- /dev/null +++ b/nebula-logger/tests/log-management/classes/LogMassDeleteExtension_Tests.cls @@ -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 logs = new List(); + 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 logs = [SELECT Id, Name FROM Log__c]; + + List logIds = new List(); + for (Log__c selectedLog : logs) { + logIds.add(selectedLog.Id); + } + + List expectedDeletableLogs = new List(); + 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 returnedDeletableLogs = extension.getDeletableLogs(); + + Test.stopTest(); + + System.assertEquals(expectedDeletableLogs.size(), returnedDeletableLogs.size()); + } + + @isTest + static void it_should_delete_selected_log_records() { + List logs = [SELECT Id, Name FROM Log__c]; + List logsToDelete = new List(); + List logsToKeep = new List(); + 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); + } + } +} diff --git a/nebula-logger/tests/log-management/classes/LogMassDeleteExtension_Tests.cls-meta.xml b/nebula-logger/tests/log-management/classes/LogMassDeleteExtension_Tests.cls-meta.xml new file mode 100644 index 000000000..d75b0582f --- /dev/null +++ b/nebula-logger/tests/log-management/classes/LogMassDeleteExtension_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 51.0 + Active +