Thursday, October 26, 2017

[Salesforce / TheMoreYouKnow] Campaign Member Status Configuration

Disclaimer
This is a "better write it down for the future me" post.

For the TL;DR pals, this is the Github repository.

It was years I wans't working with Campaigns, so it took me a while to remember a simple thing: you cannot automate
Campaign Member Status configuration with point & click
.

It seems awkward, mostly because you have a Status picklist field on the Campaign Member.

And when you cannot find the New button on the picklist related list of that field you start feeling depressed.


After a while (you don't really need much time, just google it a bit) you find out there is a wonderful Campaign Member Status object, that holds the details of a specific Campaign's members statuses.

Then you learn (again) that you need to use the Advanced Setup button (in Classic) or the Campaign Member Status related list (in LEX):



When you add a new value the list of values in the Campaign Member Status is automatically updated:


So the next question is:

How to automate this?

Well, unfortunately you can't, you have to manually create the Campaign Member Status values on every Campaign you create.

If your users create campaigns often, believe they won't accept it as a solution!


As a simple solution we can:
  • correlate Campaign Member Status values to Campaign Record Types
  • store Campaign Record Type / Campaign Member Status Value couples inside a Custom Metadata object
  • implement a trigger that executes on campaign create or if its record type changes

CampaignMemberStatusConfiguration__mdt


Name and Label standard fields will not be used to store actual values for our feature implementation, becausr they can only store 40 chars that is definitely less than 80 required for Record Type Developer name and 765 required for Campaign Member Status value (we'll be using max 255 chars in this implementation).


Campaign Record Types


Custom Metadata values


Apex Code algorithm

The following code, invoked from the Campaign trigger, calculates the new Campaign Member Status objects, removing the old one and creating the new ones.

/**
 * @author Enrico Murru (https://enree.co, @enreeco)
 * @description Creates CampaignMemberStatus records based on custom metadata object configuration
 */
public class CampaignTriggerHandler {
        
    public static void execute(){
        //new statuses to be created / updated (with default or responded)
        List<CampaignMemberStatus> defaultOrRespondedStatuses = new List<CampaignMemberStatus>();
        //new statuses to be created / updated (without default or responded)
        List<CampaignMemberStatus> otherStatuses = new List<CampaignMemberStatus>();
        //statuses to be deleted
        List<CampaignMemberStatus> deleteStatusesList = new List<CampaignMemberStatus>();
        
        //selected campaigns
        List<Campaign> cmpList = new List<Campaign>();
        //record types
        List<ID> rTypesList = new List<ID>();
        
        //select only campaigns that are inserted or that changed their record types
        for(Integer i = 0; i < Trigger.new.size(); i++){
            Campaign nCmp = (Campaign)Trigger.new[i];
            if(!Trigger.isInsert
                && nCmp.RecordTypeId == ((Campaign)Trigger.old[i]).RecordTypeId) continue;
            cmpList.add(nCmp);
            rTypesList.add(nCmp.RecordTypeId);
        }
        
        if(cmpList.isEmpty()) return;
        
        //delete standard statuses
        deleteStatusesList = [SELECT Id, Label, CampaignId, IsDefault, HasResponded 
                              From CampaignMemberStatus 
                              Where CampaignId IN :cmpList
                              Order By Label];
        
        //query record types
        Map<ID, Recordtype> rTypeMap = new Map<ID,RecordType>([Select Id, DeveloperName From RecordType 
                                                              Where SObjectType = 'Campaign'
                                                              and Id IN :rTypesList]);
     
        for(Campaign cmp : cmpList){
   
            //we can do as many query as we want with custom metadata
         for(CampaignMemberStatusConfiguration__mdt cmsc : [SELECT Id, RecordTypeDeveloperName__c, 
                                                            StatusValue__c, SortOrder__c,IsDefault__c, Responded__c 
                                                            FROM CampaignMemberStatusConfiguration__mdt
                                                            WHERE RecordTypeDeveloperName__c = :rTypeMap.get(cmp.RecordTypeId).DeveloperName
                                                            ORDER BY StatusValue__c, IsDefault__c DESC, Responded__c DESC]){

    //gets CMS with same label (avoid duplicates on upsert)
    CampaignMemberStatus oldCMS = null;
    for(Integer ci = deleteStatusesList.size()-1; ci >= 0; ci--){
                    CampaignMemberStatus cms = deleteStatusesList[ci];
     if(cms.CampaignId != cmp.Id) continue;
                    if(cms.Label == cmsc.StatusValue__c){
                        oldCMS = cms;
                        deleteStatusesList.remove(ci);
                        break;
                    }
    }
                                                                
    CampaignMemberStatus newCMS = new CampaignMemberStatus(Label = cmsc.StatusValue__c,
                                                             SortOrder = cmsc.SortOrder__c.intValue(),
                                                             IsDefault = cmsc.IsDefault__c,
                                                             HasResponded = cmsc.Responded__c);
    if(oldCMS != null){
                    newCMS.Id = oldCMS.Id;
                }else{
                    newCMS.CampaignId = cmp.Id;
                }
    if(!newCMS.IsDefault 
                   && !newCMS.HasResponded){
     otherStatuses.add(newCMS);
    }else{
                 defaultOrRespondedStatuses.add(newCMS);            
    }
   }
        }
  //this DML sequence guarantees no conflicts
        upsert defaultOrRespondedStatuses;
        delete deleteStatusesList;
        upsert otherStatuses;
        
    }
}

Find all the details in this Github repository.

Thursday, October 19, 2017

[Salesforce] ORGanizer for Salesforce is on the AppExchange!

"That's one small step for [a] man, one giant leap for mankind" (cit.)

It's with huge pride that I announce that the ORGanizer for Salesforce Chrome Extension has successfully passed the security review and it is finally on the AppExchange!


We've just celebrated 1 year since the pubblication on the Chrome Web Store and after 1 month since the request sent to the AppExchange team, we've been delivered this awesome gift.

Now we can proudly show this amazing logo:


From now on the ORGanizer for Salesforce Chrome Extension will be listed as a free app on the AppExchange.


A huge thank you to all my Ohana, who constantly supports me in this free-time project: this achievement is your achievement!

Remember that ORGanizer for Salesforce Chrome Extension is a free app, so please share your love and if you can, donate to the cause!

ORGanizer loves you all!

Monday, September 25, 2017

[Salesforce / Chrome Extension] Happy birthday to the ORGanizer: 1 year on the store!

Checking my diary agenda I could not believe: ORGanizer's first go live was exactly 365 days ago!


I wanted to share my love and expertise for Salesforce in one single Chrome Extension to be free for all, and after 1 year I receive cheers from my Salesforce pals because ORGanizer helps them successing in their daily tasks!


Here are some quick numbers:

  • 20 releases
  • users from 127 countries
  • 4000 active users ca.
  • 28 new features (7 suggested by you)
  • 47 enhancements (17 suggested by you)
  • 32 bug fix (9 suggested by you)
  • 81500 login actions in the last month
  • 222 daily logins in the last month

The analytics (recently introduced) say that the numbers are rapidly increasing and the adoption rate is getting higher and higher.

The most important number is the active users:


And indicates the number of users that day after day keep getting the ORGanizer on their Google Chrome browsers...and it is incresing day by day, and I cannot be happier!

The ORGanizer is more than the extension itself.

Trying to make it the most amazing Chrome Extension ever, there an ecosystem of side projects and systems to help me achieving this aim.

ORGanizer site

This is the central information repository for the extension.

FAQ page

A complete extension guide updated at every release.


Video guides

Support

Active support site where you can report a bug or suggest a new feature.

This is linked to my Salesforce CRM org where I store all the stuff.
It also uses an Heroku app to send the email used to confirm your identity.

Next Release page

This page contains all features and bugfix in development or developed, that will be release in the next release.
Also this feature is related to my personal ORGanizer CRM org.

Change Log

A list of all features delivered in all releases.

Donations and Swag Store

Donations link and a link to the Swag Store to get some cool ORGanizer swag to allow me keeping the extension free for all.

Live Messages

Live messages to get live data to users about the extension, like unexpected bugs or general info.
I use ORGanizer like you do, so I wanted to put in place a feature to communicate with my ORGanusers.

Online reviews
I received amazing reviews from Salesforce community leaders:

Who made it?
I'm the only person behind the ORGanizer but my dear friend Davide D'Annibale is helping me with all the graphics (I litteraly have not taste in graphics :) ) to get it the most professional appearance it can have!

Amazing feedbacks
What's the best way to thank all supporters? Let's show some of the best tweets I got from the web!