Welcome to another Salesforce automation tutorial.
As usual, I will explain the challenge and ‘criticise’ Salesforce for not having a built-in solution. Of cause, in the end, a useful custom solution for this problem will be provided.
Let’s get started.
Challenge: Salesforce comes with a pretty cool approval tool – approval process, it handles most of the business needs, however, I feel like its missing one of the most important features – attaching files with the approval email.
Since there is no way to hack the standard feature, I decided to explore “Salesforce Files” instead of “Salesforce Attachments”. (Yes, they are different)
What I have noticed is that Salesforce files can be shared externally via public facing links, maybe we can include this in the approval email to allow approvers to view/download files similar to how people download email attachments.
To achieve this, we are going to need:
- A custom field to hold the links
- An HTML email template to include the custom field
- An apex trigger to:
- create an external link for each attachment
- update the custom field with the new links
- remove links when files are deleted
A custom field is really easy, just create a custom field under the object with type = rich text area.
Next step, we create an email template that includes this new field. (Too easy, not going to include any screenshots…)
Now the fun part, apex trigger.
After going through the documentation, I realised it’s not as simple as I imagined. This apex trigger not only takes care of generating and removing public links but also writes the links back to our custom field in HTML format.
List<ID> ContentDocumentIDs = new List<ID>();
List<ID> yourObjectIDs = new List<ID>();
/* ====== creating public facing urls for files after link insert ====== */
if (Trigger.isInsert){
String tmp;
// adding qualified files to the list - YourObject
for (ContentDocumentLink cdl : Trigger.New){
tmp = cdl.LinkedEntityId;
// need to use Left to build links only for related objects (e.g. opportunity)
if (tmp != null && tmp.left(3) == 'xxx'){
ContentDocumentIDs.add(cdl.ContentDocumentId);
yourObjectIDs.add(tmp);
}
}
// list of content documents
List<ContentDocument> cdList = [
select id, Title, LatestPublishedVersionId
from ContentDocument
where id in: ContentDocumentIDs
];
// list of public links
List<ContentDistribution> cds = new List<ContentDistribution>();
for (ContentDocument c : cdList){
ContentDistribution cd = new ContentDistribution(
name = c.Title + ' - ' + date.today().format(),
ContentVersionId = c.LatestPublishedVersionId
);
cdList.add(cd);
}
// creating links
insert cdList;
}
else if (Trigger.isDelete){
System.debug('YourObject Content Sharing Trigger Starts');
String tmp;
// adding qualified files to the list - YourObject
for (ContentDocumentLink cdl : Trigger.Old){
tmp = cdl.LinkedEntityId;
if (tmp != null && tmp.left(3) == 'a1V'){
ContentDocumentIDs.add(cdl.ContentDocumentId);
yourObjectIDs.add(tmp);
}
}
delete [
select id, Title, LatestPublishedVersionId
from ContentDocument
where id in: ContentDocumentIDs
];
}
/* ====== Now writing links back to Your Objects ====== */
if (!yourObjectIDs.isEmpty()){
System.debug('Writting back to Your Objects');
// reset attachment list in Your Objects
List<ContentDocumentLink> ContentDocumentLinks = [
select id, LinkedEntityId, ContentDocumentId
from ContentDocumentLink
where LinkedEntityId in :yourObjectIDs
];
System.debug('ContentDocumentLinks: ' + ContentDocumentLinks);
// Your Object ID -> ContentDocument IDs
Map<ID, List<ID>> ContentDocumentMap = new Map<ID, List<ID>>();
List<ID> ContentDocumentIds = new List<ID>();
for (ContentDocumentLink ContentDocumentLink : ContentDocumentLinks){
ContentDocumentIds.add(ContentDocumentLink.ContentDocumentId);
if (ContentDocumentMap.containsKey(ContentDocumentLink.LinkedEntityId)){
ContentDocumentMap.get(ContentDocumentLink.LinkedEntityId).add(ContentDocumentLink.ContentDocumentId);
}
else{
ContentDocumentMap.put(ContentDocumentLink.LinkedEntityId, new List<ID>{ContentDocumentLink.ContentDocumentId});
}
}
List<ContentDocument> ContentDocuments = [
select id, Title, LatestPublishedVersionId
from ContentDocument
where id in: ContentDocumentIds
];
System.debug('ContentDocuments: ' + ContentDocuments);
List<ID> LatestPublishedVersionIds = new List<ID>();
for (ContentDocument ContentDocument : ContentDocuments){
LatestPublishedVersionIds.add(ContentDocument.LatestPublishedVersionId);
}
List<ContentDistribution> ContentDistributions = [
select id, Name, DistributionPublicUrl, ContentDocumentId, ContentVersionId
from ContentDistribution
where ContentVersionId in: LatestPublishedVersionIds
];
System.debug('ContentDistributions: ' + ContentDistributions);
// ContentDocumentId -> ContentDistribution
Map<ID, ContentDistribution> ContentDistributionMap = new Map<ID, ContentDistribution>();
for (ContentDistribution ContentDistribution : ContentDistributions){
ID tmpDocId = ContentDistribution.ContentDocumentId != null ? ContentDistribution.ContentDocumentId : ContentDistribution.ContentVersionId;
ContentDistributionMap.put(tmpDocId, ContentDistribution);
}
// Your Object ID -> List<ContentDistribution>
Map<ID, List<ContentDistribution>> pMap = new Map<ID, List<ContentDistribution>>();
List<Purchase_Order__c> Your Objects = [
select id, Attachment_List__c
from purchase_order__c
where id in: yourObjectIDs
];
System.debug('Your Objects: ' + Your Objects);
String attachmentString = '';
For (Purchase_Order__c YourObject : Your Objects){
if (ContentDocumentMap != null && ContentDocumentMap.size() > 0){
for (ID ContentDocumentId : ContentDocumentMap.get(YourObject.id)){
attachmentString += '
<li><a href="' + ContentDistributionMap.get(ContentDocumentId).DistributionPublicUrl + '">' + ContentDistributionMap.get(ContentDocumentId).Name + '</a></li>
';
}
}
YourObject.Attachment_List__c = attachmentString;
}
update Your Objects;
/* ====== Now writing links back to Your Objects ====== */
if (!yourObjectIDs.isEmpty()){
System.debug('Writting back to Your Objects');
// reset attachment list in Your Objects
List<ContentDocumentLink> ContentDocumentLinks = [
select id, LinkedEntityId, ContentDocumentId
from ContentDocumentLink
where LinkedEntityId in :yourObjectIDs
];
System.debug('ContentDocumentLinks: ' + ContentDocumentLinks);
// Your Object ID -> ContentDocument IDs
Map<ID, List<ID>> ContentDocumentMap = new Map<ID, List<ID>>();
List<ID> ContentDocumentIds = new List<ID>();
for (ContentDocumentLink ContentDocumentLink : ContentDocumentLinks){
ContentDocumentIds.add(ContentDocumentLink.ContentDocumentId);
if (ContentDocumentMap.containsKey(ContentDocumentLink.LinkedEntityId)){
ContentDocumentMap.get(ContentDocumentLink.LinkedEntityId).add(ContentDocumentLink.ContentDocumentId);
}
else{
ContentDocumentMap.put(ContentDocumentLink.LinkedEntityId, new List<ID>{ContentDocumentLink.ContentDocumentId});
}
}
List<ContentDocument> ContentDocuments = [
select id, Title, LatestPublishedVersionId
from ContentDocument
where id in: ContentDocumentIds
];
System.debug('ContentDocuments: ' + ContentDocuments);
List<ID> LatestPublishedVersionIds = new List<ID>();
for (ContentDocument ContentDocument : ContentDocuments){
LatestPublishedVersionIds.add(ContentDocument.LatestPublishedVersionId);
}
List<ContentDistribution> ContentDistributions = [
select id, Name, DistributionPublicUrl, ContentDocumentId, ContentVersionId
from ContentDistribution
where ContentVersionId in: LatestPublishedVersionIds
];
System.debug('ContentDistributions: ' + ContentDistributions);
// ContentDocumentId -> ContentDistribution
Map<ID, ContentDistribution> ContentDistributionMap = new Map<ID, ContentDistribution>();
for (ContentDistribution ContentDistribution : ContentDistributions){
ID tmpDocId = ContentDistribution.ContentDocumentId != null ? ContentDistribution.ContentDocumentId : ContentDistribution.ContentVersionId;
ContentDistributionMap.put(tmpDocId, ContentDistribution);
}
// Your Object ID -> List<ContentDistribution>
Map<ID, List<ContentDistribution>> pMap = new Map<ID, List<ContentDistribution>>();
List<Purchase_Order__c> Your Objects = [
select id, Attachment_List__c
from purchase_order__c
where id in: yourObjectIDs
];
System.debug('Your Objects: ' + Your Objects);
String attachmentString = '';
For (Purchase_Order__c YourObject : Your Objects){
if (ContentDocumentMap != null && ContentDocumentMap.size() > 0){
for (ID ContentDocumentId : ContentDocumentMap.get(YourObject.id)){
attachmentString += '
<li><a href="' + ContentDistributionMap.get(ContentDocumentId).DistributionPublicUrl + '">' + ContentDistributionMap.get(ContentDocumentId).Name + '</a></li>
';
}
}
YourObject.Attachment_List__c = attachmentString;
}
update Your Objects;
There you have it, a long, complicated, hard-to-read apex code. The first bit creates and deletes public links. The second long part adds the newly created links to our custom field created above – Attachment_List__c.
Finally, we add the File related list to the page layout. (Remove Attachments if you wish)
Here is what it looks like when a file is uploaded.
(File uploading page)
The Attachment List custom field looks like an unordered list, which is exactly what it looks like in the approval email. Approvers will now be able to click on the link and access the file without logging into Salesforce, how convenient is that.
The external link also provides a useful download button if the file cannot be rendered by your browser.
That’s all for today, hope you enjoy it.