diff --git a/.forceignore b/.forceignore index eeed72a8..bffd4f14 100644 --- a/.forceignore +++ b/.forceignore @@ -2,4 +2,7 @@ **/*.ts **/tsconfig*.json **/*.tsbuildinfo -**/eslint.config.mjs \ No newline at end of file +**/eslint.config.mjs +**/jsconfig.json + +**/.eslintrc.json diff --git a/.gitignore b/.gitignore index 9de1c41a..23990ae2 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,6 @@ target/ node_modules/ /.illuminatedCloud/ **/tsconfig*.json -**/*.tsbuildinfo \ No newline at end of file +**/*.tsbuildinfo +package-lock.json +package.json diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls new file mode 100644 index 00000000..b1c63cd1 --- /dev/null +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -0,0 +1,8 @@ +public with sharing class SummitEventsLinkHandler { + public static void run(Map newMap) { + new SummitEventsRtfLinkPipeline(newMap).run(); + } + + // Future consideration: add async logic here + +} \ No newline at end of file diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml b/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml new file mode 100644 index 00000000..82775b98 --- /dev/null +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 65.0 + Active + diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls new file mode 100644 index 00000000..3394e84d --- /dev/null +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -0,0 +1,283 @@ +public without sharing class SummitEventsRtfLinkPipeline { + + // Inelegant, but schema introspection can't differentiate RTF from regular text fields + private static final Set RICHTEXT_FIELDS = new Set{ + 'Event_Full_Text__c', 'Event_Confirmation_Description__c', + 'Event_Additional_Questions_Description__c', + 'Event_Appointment_Description__c', 'Event_Footer__c', + 'Event_Submit_Description__c', 'Event_Cancel_Review_Description__c', + 'Event_Payment_Due_Description__c', 'Event_Payment_Received_Description__c', + 'Event_description__c', 'Donation_Description__c', + 'Guest_Registration_Description__c' + }; + + private static final String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); + + private Map cvIdToCDMap; + private List wrappedEventList; + private List idList; + private String executionStage; + + public SummitEventsRtfLinkPipeline(Map newMap) { + this.cvIdToCDMap = new Map(); + this.wrappedEventList = new List(); + this.idList = new List(newMap.keySet()); + } + + public void run() { + this.executionStage = 'run'; + try { + filter(); + scan(); + prepare(); + create(); + push(); + } catch(Exception e) { + System.debug('Pipeline failed at stage ' + this.executionStage + ': ' + e.getMessage()); + } + } + + /** + * Cheap first-pass filter to select and wrap Summit_Events__c records + * that have RTF fields with links to ContentVersion records. + */ + @TestVisible + private void filter() { + + this.executionStage = 'filter'; + + List freshEventList = new List(); + List fields = new List(); + String query = ''; + + Summit_Events__c schemaObject = new Summit_Events__c(); + Map fieldMap = + schemaObject + .getSObjectType() + .getDescribe() + .fields + .getMap(); + + for(String field : RICHTEXT_FIELDS) { + + Boolean isUpdateable = fieldMap.get(namespace + field).getDescribe().isUpdateable(); + + if(isUpdateable) { + fields.add(namespace + field); + } + } + + if(fields.isEmpty()) { + throw new SummitEventsException('No RTF fields found or user lacks sufficient permissions to update fields'); + } + + String fieldList = String.join(fields, ', '); + query = 'SELECT Name, ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :idList'; + freshEventList = Database.query(query); + + for(Summit_Events__c freshEvent : freshEventList) { + SummitEventWrapper wrappedEvent = new SummitEventWrapper(); + for (String field : fields) { + String fieldVal = (String) freshEvent?.get(field); + if(!String.isBlank(fieldVal) && fieldVal.contains('> tempMap = new Map>(); + + for (String field : wrappedEvent.fieldsWithLinks) { + String html = (String) wrappedEvent.event.get(field); + List items = new List(); + + Integer pos = 0; + while (true) { + // Find the next + Integer tagStart = html.indexOf(' cvIdSet = new Set(); + for(SummitEventWrapper wrappedEvent : wrappedEventList) { + for(String field : wrappedEvent.fieldsToLinkMap.keySet()) { + for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) { + cvIdSet.add(sfi.cvId); + } + } + } + + List cdList = [ + SELECT Id, + DistributionPublicUrl, + Name, + ContentVersionId + FROM ContentDistribution + WHERE ContentVersionId IN :cvIdSet + ]; + + for(ContentDistribution cd : cdList) { + this.cvIdToCDMap.put(cd.ContentVersionId, cd); + } + } + + /** + * Create ContentDistribution records for any ContentVersion records + * that are not already linked to a ContentDistribution record. + */ + @TestVisible + private void create() { + + this.executionStage = 'create'; + + List cdInsertList = new List(); + Set visited = new Set(); + + for(SummitEventWrapper wrappedEvent : wrappedEventList) { + for(String field : wrappedEvent.fieldsToLinkMap.keySet()) { + Integer i = 0; + for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) { + if(!cvIdToCDMap.containsKey(sfi.cvId) && !visited.contains(sfi.cvId)) { + ContentDistribution cd = new ContentDistribution(); + cd.ContentVersionId = sfi.cvId; + cd.Name = wrappedEvent.event.Name + ' - ' + field + i++; + cdInsertList.add(cd); + visited.add(sfi.cvId); + } + } + } + } + + if(!cdInsertList.isEmpty()) { + insert cdInsertList; + + List refreshed = [ + SELECT Id, + DistributionPublicUrl, + Name, + ContentVersionId + FROM ContentDistribution + WHERE Id IN :cdInsertList + ]; + + for(ContentDistribution cd : refreshed) { + cvIdToCDMap.put(cd.ContentVersionId, cd); + } + } + } + + /** + * Replace all URLs in the RTF fields with the corresponding ContentDistribution + * record's DistributionPublicUrl, and update the Summit_Events__c record. + */ + @TestVisible + private void push() { + + this.executionStage = 'push'; + + List updateList = new List(); + + for(SummitEventWrapper wrappedEvent : wrappedEventList) { + Summit_Events__c tempEvent = new Summit_Events__c(); + tempEvent.Id = wrappedEvent.event.Id; // trigger records are immutable, we must create new records + for(String field : wrappedEvent.fieldsWithLinks) { + String rtf = (String) wrappedEvent.event.get(field); + for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) { + rtf = rtf.replace(sfi.url, cvIdToCDMap.get(sfi.cvId).DistributionPublicUrl); + } + tempEvent.put(field,rtf); + } + updateList.add(tempEvent); + } + + if(!updateList.isEmpty()) { + this.executionStage = 'push.DML'; + update updateList; + } + } + + // Simple wrapper to aggregate relevant values we'll use + private class SummitEventWrapper { + Summit_Events__c event; + Set fieldsWithLinks; + Map> fieldsToLinkMap; + + private SummitEventWrapper() { + this.fieldsWithLinks = new Set(); + this.fieldsToLinkMap = new Map>(); + } + } + + private class SummitFieldItem { + String url; + Id cvId; + private SummitFieldItem() {} + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls-meta.xml b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls-meta.xml new file mode 100644 index 00000000..82775b98 --- /dev/null +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls-meta.xml @@ -0,0 +1,5 @@ + + + 65.0 + Active + diff --git a/force-app/main/default/objects/Summit_Events_Settings__c/fields/Opt_Into_Link_Automation__c.field-meta.xml b/force-app/main/default/objects/Summit_Events_Settings__c/fields/Opt_Into_Link_Automation__c.field-meta.xml new file mode 100644 index 00000000..d3165e8b --- /dev/null +++ b/force-app/main/default/objects/Summit_Events_Settings__c/fields/Opt_Into_Link_Automation__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Opt_Into_Link_Automation__c + false + Opt into automatic creation of public links backed by ContentVersion files found in Summit Events RTFs. + false + Opt into automatic creation of public links backed by ContentVersion files found in Summit Events RTFs. + + false + Checkbox + diff --git a/force-app/main/default/objects/Summit_Events_Settings__c/fields/Turn_off_Summit_Events_Trigger__c.field-meta.xml b/force-app/main/default/objects/Summit_Events_Settings__c/fields/Turn_off_Summit_Events_Trigger__c.field-meta.xml new file mode 100644 index 00000000..da9e5c72 --- /dev/null +++ b/force-app/main/default/objects/Summit_Events_Settings__c/fields/Turn_off_Summit_Events_Trigger__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Turn_off_Summit_Events_Trigger__c + false + Turns off Summit Event App's Summit Events trigger. + false + Turns off Summit Event App's Summit Events trigger. + + false + Checkbox + diff --git a/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger new file mode 100644 index 00000000..907fa3e1 --- /dev/null +++ b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger @@ -0,0 +1,13 @@ +trigger SummitEventsSummitEventTrigger on Summit_Events__c (before insert, before update, after insert, after update) { + Summit_Events_Settings__c SummitEventsSettings = Summit_Events_Settings__c.getOrgDefaults(); + if (!SummitEventsSettings.Turn_off_Summit_Events_Trigger__c) { + if (SummitEventsSettings.Opt_Into_Link_Automation__c) { + if (Trigger.isInsert && Trigger.isAfter) { + SummitEventsLinkHandler.run(Trigger.newMap); + } + if (Trigger.isUpdate && Trigger.isAfter) { + SummitEventsLinkHandler.run(Trigger.newMap); + } + } + } +} \ No newline at end of file diff --git a/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger-meta.xml b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger-meta.xml new file mode 100644 index 00000000..260ec509 --- /dev/null +++ b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls new file mode 100644 index 00000000..edfe34bb --- /dev/null +++ b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls @@ -0,0 +1,48 @@ +@IsTest +public with sharing class SummitEventsRtfLinkPipeline_TEST { + + @TestSetup + static void makeData() { + ContentVersion cv = new ContentVersion( + Title = 'Test', + PathOnClient = 'test.txt', + VersionData = Blob.valueOf('Test.jpg'), + IsMajorVersion = true + ); + insert cv; + } + + @IsTest + static void testLinkReplacement() { + + ContentVersion cv = [SELECT Id FROM ContentVersion LIMIT 1]; + String namespace = SummitEventsNamespace.getNamespace(); + + String rtfUrl = '

test

'; + + Summit_Events__c se = new Summit_Events__c(); + se.put(namespace + 'Event_Full_Text__c', rtfUrl); + se.put(namespace + 'Event_Name__c', 'Test Event'); + se.put(namespace + 'Event_Status__c', 'Active'); + se.put(namespace + 'Name', 'Test Event'); + insert se; + se = [SELECT Event_Full_Text__c,Event_Name__c,Event_Status__c,Name FROM Summit_Events__c WHERE Id = :se.Id]; + + + Map newMap = new Map{ se.Id => se }; + + Test.startTest(); + SummitEventsRtfLinkPipeline rtfp = new SummitEventsRtfLinkPipeline(newMap); + rtfp.run(); + Test.stopTest(); + + Summit_Events__c updated = [ + SELECT Event_Full_Text__c,Event_Name__c,Event_Status__c,Name FROM Summit_Events__c WHERE Id = :se.Id + ]; + ContentDistribution cd = [SELECT Id, DistributionPublicUrl FROM ContentDistribution WHERE ContentVersionId = :cv.Id LIMIT 1]; + System.assert(!updated.Event_Full_Text__c.contains('versionId=068')); + System.assert(updated.Event_Full_Text__c.contains('https://')); + System.assert(updated.Event_Full_Text__c.contains(cd.DistributionPublicUrl)); + } +} diff --git a/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls-meta.xml b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls-meta.xml new file mode 100644 index 00000000..82775b98 --- /dev/null +++ b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 65.0 + Active +