Link Chatter Files to Related Objects

I prefer to upload “attachments” to records using Chatter Files vs. the old skool Attachments because…

  • Chatter Files are searchable
  • They still get listed under the Notes & Attachments list for a record
  • I get the nice file previewer (most of my files are PDFs)
  • I can store them once and link them to anything else

It’s that last point that is especially cool and cannot be done with regular Attachments. “Store Once, Link Anywhere

One major difference, though, between old skool Attachments and Chatter Files is that Attachments would get linked up to the Parent Records too, at least for Standard Objects. This was nice. I could add an Attachment to an Opportunity and see it listed under the Account without needing to do anything special. Chatter Files do not do this. You can manually make it do this, but it’s not automated.

Let’s automate this missing feature!

Trigger
Simple little trigger to gather IDs and call an @future event in a class. Note that the call is wrapped in the IF statment checking to see if we are already in an @future or batch context. For any trigger that calls an @future event, please always wrap the call like this.

trigger FeedItems on FeedItem (after insert) {

	// AFTER INSERT
	if(Trigger.isAfter && Trigger.isInsert){
		
		List<ID> IDs = new List<ID>();
		for (FeedItem fi : trigger.new){
			IDs.add(fi.id);
		}
		
		if(!system.isFuture() && !system.isBatch()){
			FeedItems.linkChatterFiles(IDs);
		}
		
    }
	
}

Apex Class
I have the method written to only process a single ContentPost (assuming the UI is being used), but if you want to bulkify, be my guest.

What we are doing in the class is gathering the IDs of records we also want to link the file to. For me, I want to link files I post to Opportunities to the parent Account and files I post to Cases to the related Contact and Account. It’s up to you what to link.

One important step in there is to gather a list of records the file is already linked to. Towards the end of the code, we remove those IDs from the master list so we do not link them all over again with a new Chatter Post.

Take note of the InProcess boolean. This is a technique to prevent recursion. Since our trigger is on FeedItem and our method creates more FeedItems, we don’t want the trigger to keep firing.

public class FeedItems {

    public static Boolean inProcess = false;
	
    // Links Chatter Files on certain objects to related objects
    @future 
    public static void linkChatterFiles(List<Id> IDs) {
    	
    	// Only run for individual posts of files
    	if(IDs.size() == 1 && inProcess == false){
    		
    		inProcess = true; // turn on so we don't do recursive loops
    		List<FeedItem> FIsToInsert = new List<FeedItem>(); // list we will be inserting
    		
	    	for (FeedItem f: [select id, type, RelatedRecordId, parentId, title, body from FeedItem where id in:IDs and type = 'ContentPost']){
				
				// Verify fields that we cannot use to filter SOQL
				if (f.RelatedRecordId == null) { continue; }
				
				// Get ContentVerion record
				ContentVersion cv = [select id, ContentDocumentId from ContentVersion where id = :f.RelatedRecordId][0];
				
				// Get list of IDs this Content is already linked to so we don't do duplicate posts
				Set<Id> IDsAlreadyLinked = new Set<Id>();
				for (ContentDocumentLink cdl : [SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink where ContentDocumentId = :cv.ContentDocumentId]){
					
					IDsAlreadyLinked.add(cdl.LinkedEntityId);
					
				}
				
				// Depending on the object, link the content to different other objects
				Set<Id> IDsToLink = new Set<Id>();
				if (String.valueOf(f.parentId).startsWith( Opportunity.sObjectType.getDescribe().getKeyPrefix() )){
					
					Opportunity o = [select id, accountId from Opportunity where id = :f.ParentId limit 1][0];
					if(o.AccountId != null){ 
						IDsToLink.add(o.AccountId);
					}
					
					
				} else if (String.valueOf(f.parentId).startsWith( Case.sObjectType.getDescribe().getKeyPrefix() )){
					
					Case c = [select id, accountId, contactId from Case where id = :f.ParentId limit 1][0];
					if(c.AccountId != null){ 
						IDsToLink.add(c.AccountId);
					}
					if(c.contactId != null){ 
						IDsToLink.add(c.contactId);
					}
					
				}
				
				// Remove the IDs that are already linked so we don't create duplicate posts 
				IDsToLink.removeAll(IDsAlreadyLinked);
				
				// Create a new FeedItem for each of the records we want to link to
				for (ID theId : IDsToLink){
					FeedItem newFI = new FeedItem();
						newFI.Type = 'ContentPost';
						newFI.RelatedRecordId = f.RelatedRecordId;
						newFI.ParentId = theId;
						newFI.title = f.title;
						newFI.Body = f.body;
					FIsToInsert.add(newFI);
					
				}
				
			} // end main loop
		
			// INSERT
			if (!FIsToInsert.isEmpty()){ database.insert(FIsToInsert, false); }
				
    	} // end check for 1 record in trigger
    	
    } // end of linkChatterFiles method
    
}

Test
And the Test code. We have a separate method for each object we want to test posting a file to. I should have some asserts in there to test it actually works, but I don’t have those yet. That’s your homework.

@isTest 
private class FeedItems_Test {

    static testMethod void linkChatterFiles_Oppty() {
    	
    	Account a = new Account(name='test');
    	insert a;
    	
    	Contact c = new Contact (FirstName = 'Joe', LastName = 'Shmo', AccountId = a.id);
    	insert c;
    	
    	OpportunityStage stage = [select MasterLabel from OpportunityStage where IsClosed = false limit 1];
    	
    	Opportunity o  = new Opportunity();
        o.Name         = 'TEST'; 
        o.AccountId    = a.id;
        o.CloseDate    = Date.today(); 
        o.StageName    = stage.masterlabel;
        insert o;
        
        ContentVersion cv = new ContentVersion(); 
		cv.Origin = 'H';
		cv.PathOnClient='myFile.txt';
		cv.Title ='myFile'; 
		cv.VersionData = Blob.valueOf('I am a file posting to Chatter');
		insert cv;
		
		FeedItem contentFI = new FeedItem();
		contentFI.Type = 'ContentPost';
		contentFI.ParentId = o.id; // Opportunity
		contentFI.RelatedRecordId = cv.id;
		contentFI.title = 'Content Post';
		contentFI.Body = 'Body of content post';
		insert contentFI;
		
    }
    
    static testMethod void linkChatterFiles_Case() {
    	
    	Account a = new Account(name='test');
    	insert a;
    	
    	Contact c = new Contact (FirstName = 'Joe', LastName = 'Shmo', AccountId = a.id);
    	insert c;
    	
    	Case cs = new Case();
        cs.subject = 'Test';
        cs.ContactId = c.id;
        cs.AccountId = a.id;
        insert cs;
        
        ContentVersion cv = new ContentVersion(); 
		cv.Origin = 'H';
		cv.PathOnClient='myFile.txt';
		cv.Title ='myFile'; 
		cv.VersionData = Blob.valueOf('I am a file posting to Chatter');
		insert cv;
		
		FeedItem contentFI = new FeedItem();
		contentFI.Type = 'ContentPost';
		contentFI.ParentId = cs.id; // Case
		contentFI.RelatedRecordId = cv.id;
		contentFI.title = 'Content Post';
		contentFI.Body = 'Body of content post';
		insert contentFI;
		
    }
        
}

Enjoy!

1 Comment »

  1. phil vasey Said,

    March 28, 2013 @ 2:23 am

    Hi Scott

    In your unit test linkChatterFiles_Oppty(), try adding

    delete o ;

    at the end.

RSS feed for comments on this post · TrackBack URI

Leave a Comment

All comments are moderated. Other visitors will not see your comment until it has been approved.