06 October 2012

Show all components on SFDC Change Set Component List



What’s annoying in creating outbound Change Set is that the component list does not remember previously selected component when navigating thru pages.


For example on the outbound change set component list:

1. I'm adding a custom field on the first page



2. I navigate to the next page and then selected another custom field.



3. When I click the 'Add to Change Set' only the last field was added.


It's counter productive because if you have a huge amount of components spread across different pages this would mean a lot of clicks.

However you can hack the URL to display all components. Here's the steps (as of summer '12):

  1. Assuming you already created the base Change Set and click the 'Add' button and selected the Component type. You need to click on the 'more' link at the bottom of the change set component list.
  2. When the page refreshes modify the browser URL such that the parameter rowsperpage parameter equal to amount of components you want to display (Usually around 1000 would work to display all components unless your Instance really has a lot of stuff).

That's it! You will be able to see at least a large set of components and you don't need to navigate or experience the hassle that SFDC doesn't save your previous selection.

Note that this is also the same URL hacks you can use for sharing rules list etc. :-)






17 September 2012

Generic Salesforce Search Popup Screen

Sometime ago I created a POC on creating a generic search popup in which I want the code to be flexible such that I can specify the fields and object that I want the search popup to be able to search onto.

Immediately I thought of using Visualforce Dynamic Binding (Dynamic Visualforce Component not yet available at that time),  dynamic SOQL and Visualforce Component and was able to create a working prototype.

Also I search the net to know the best and easiest way to override the search popup window without me inventing the wheel and found this blog from Jeff Douglas which is cool.  Anyway I wanted to share the code to everyone who might need it and if possible improve it.

Please note that this is not the best code as this is just a prototype. :-)

Visualforce Component

<apex:component controller="GenericSearchComponentCntlr">
     <apex:attribute name="columns_displayed" 
          description="Comma delimited field name we want to 
               display on the table. Don't include the 'Id' 
               as it retrieve internally." 
               type="string" required="true" 
          assignTo="{!sColumnNames}"/>
     <apex:attribute name="object_name" 
          description="The SObject we want to search." 
          type="string" 
          required="true" 
          assignTo="{!sSObjectName}"/>
     <apex:attribute name="additional_custom_filter" 
          description="the custom SOQL filter we want to add 
              to the query logic." 
          type="string" 
          required="false" 
          assignTo="{!sCustomFilter}"/>
     <apex:form id="searchForm">
         <!-- Search Buttons -->
         <apex:outputPanel style="margin:5px;padding:10px;
              padding-top:2px;">
              <strong>Search</strong>&nbsp;
              <apex:inputText value="{!searchString}"/>
              &nbsp;
              <apex:commandButton value="Go" action="{!search}"/>
         </apex:outputPanel>
        
         <apex:outputPanel layout="block" 
              style="margin:5px;padding:10px;padding-top:2px;">
              <apex:outputPanel id="searchSection">
                   <!-- Search Table -->
                   <apex:outputPanel >
                       <apex:pageBlock title="Search Results">
                           <apex:pageBlockTable 
                               value="{!oDisplayRecords}" 
                               var="rec" 
                               rendered="{!hasData}">
                               <apex:repeat value="{!oSearchColumnDetails}" 
                                    var="colDesc">
                                    <apex:column>
                        <apex:facet name="header">  
                                             <apex:outputText 
                                                  value="{!colDesc.dispName}"/>         
                                        </apex:facet>
                                     
                                        <apex:outputText 
                                            value="{!rec[colDesc.apiName]}" 
                                            rendered="{!IF(colDesc.apiName&lt;>
                                            'Name',TRUE,FALSE)}"
                                        />
                                                            
                                        <apex:outputLink 
                                            value="javascript:top.window.
                                            opener.lookupPick2('{!FormTag}',
                                            '{!TextBox}_lkid',
                                            '{!TextBox}',
                                            '{!rec.Id}',
                                            '{!rec[colDesc.apiName]}', 
                                            false)" 
                                            rendered="{!IF(colDesc.apiName
                                            ='Name',TRUE,FALSE)}">
                                            {!rec[colDesc.apiName]}
                                        </apex:outputLink>
                                
                                     </apex:column>
                               </apex:repeat>
                         </apex:pageBlockTable>
                        
                        <apex:outputPanel rendered="{!NOT(hasData)}">
                            <center>
                                 <apex:outputText value="{!sTableMsg}"/>
                            </center>
                        </apex:outputPanel>
                    </apex:pageBlock>
               </apex:outputPanel>
          </apex:outputPanel>
       </apex:outputPanel>
     </apex:form>
</apex:component>

Apex Controller

public class GenericSearchComponentCntlr {
    static string COMMA_SEPARATOR = ',';
    static string SEMICOLON_SEPARATOR = ';';
    static integer INITIAL_LIMIT = 10;
    
    // Component parameters
    public string sColumnNames {get;set;}
    public string sSObjectName {get;set;}
    public string sCustomFilter {get;set;}
    
    // Page variables
    public list<sObject> oDisplayRecords {get;set;}
    public list<ColumnDetail> oSearchColumnDetails{get;set;}
    public string searchString {get;set;}
    
    public string sTableMsg {get;set;}
    
    // Internal variables
    Schema.SObjectType sObjType;
    Schema.SObjectField sObjQuickCreateParentField;
    
    // Used to send the link to the right dom element
    public string getFormTag() {
        return System.currentPageReference().getParameters().get('frm');
    }
 
    // Used to send the link to the right dom element for the text box
    public string getTextBox() {
        return System.currentPageReference().getParameters().get('txt');
    }
            
    public GenericSearchComponentCntlr(){
        
        searchString = system.currentPageReference().getParameters().get('lksrch');
        system.debug('searchString: ' + searchString);
        sTableMsg = 'Searching...';
    }
    
    public PageReference search(){
        if(oSearchColumnDetails==null){
            createColumnDetails();
        }
        
        system.debug('sColumnNames: ' + sColumnNames);
        system.debug('sSObjectName: ' + sSObjectName);
        
        string soql = 'select Id, ' + sColumnNames + 
                      ' from ' + sSObjectName + 
                      ' where (name like \'%' + 
                        searchString + '%\')';
        if(sCustomFilter!=null)
            soql += (' and ' + sCustomFilter);
        system.debug('soql: ' + soql);
        
        try{
            oDisplayRecords = database.query(soql);
            if(oDisplayRecords.size()==0)
                sTableMsg = 'No record found.';
        }catch(Exception e){
            system.debug('ERROR: ' + e.getMessage());
        }
        return null;
    }
    
    public boolean gethasData(){
        boolean retVal = false;
        system.debug('hasData');
        if(oDisplayRecords!=null){
            if(oDisplayRecords.size()>0){
                retVal = true;
            }
        }
        
        system.debug('hasData retVal: ' + retVal);
        return retVal;
    }
        
    private void createColumnDetails(){
        // Describe SObject
        Map<String,Schema.SObjectType> globalDesc = Schema.getGlobalDescribe();
        sObjType = globalDesc.get(sSObjectName);
        
        // Describe SObject fields
        Schema.DescribeSObjectResult desSObjResult = sObjType.getDescribe();
        Map<String,Schema.SObjectField> sObjFields = desSObjResult.fields.getMap();
        
        // Process Search Fields only when quick create is enable.
        oSearchColumnDetails = new list<ColumnDetail>();
        list<string> columnNames = sColumnNames.split(COMMA_SEPARATOR);
        
        for(string fieldName : columnNames){
            string cleanFieldName = fieldName.trim();
                                
            if(sObjFields.containsKey(cleanFieldName)){
                Schema.SObjectField field = sObjFields.get(cleanFieldName);
                Schema.DescribeFieldResult fieldDesc = field.getDescribe();   
                 
                ColumnDetail newColumn = new ColumnDetail(fieldDesc.getName(),fieldDesc.getLabel());
                oSearchColumnDetails.add(newColumn);    
            }
        }
    }
    
    private class ColumnDetail{
        public string apiName {get;set;}
        public string dispName {get;set;}   
        public boolean isRequired {get;set;}
        
        public ColumnDetail(string apiName, string dispName){
            this.apiName = apiName;
            this.dispName = dispName;
            this.isRequired = false;
        }
    }
}

Visualforce Page


<apex:page title="Search" showHeader="false" 
     sideBar="false" tabStyle="Account" id="pg">
     <!-- Use the generic search component -->
     <c:GenericSearchComponent columns_displayed="Name, Phone, Description" 
        object_name="Account"
        additional_custom_filter="{!$CurrentPage.parameters.filter}"
    />
</apex:page>

How to use the code (Only on custom pages)

In your Visualforce page, you need to override the salesforce javascript method that calls the standard popup page as per the earlier blog mentioned. So for example here is how I did it:

Visualforce Page

<apex:page standardController="Contact">
     <script type="text/javascript">
     // This function overrides the sfdc lookup popup call.
     function openLookup(baseURL, width, modified, searchParam){
          var originalbaseURL = baseURL;
          var originalwidth = width;
          var originalmodified = modified;
          var originalsearchParam = searchParam;
 
          var lookupType = baseURL.substr(baseURL.length-3, 3);
          if(modified == '1') 
               baseURL = baseURL + searchParam;
               var isCustomLookup = false;
 
               // Following "001" is the lookup type for Account 
               // object so change this as per your standard or custom object
               if(lookupType == "001"){
                  var urlArr = baseURL.split("&");
                  var txtId = '';
                
                  if(urlArr.length > 2) {
                      urlArr = urlArr[1].split('=');
                      txtId = urlArr[1];
                  }
 
                 // Following is the url of Custom Lookup page. 
                 // You need to change that accordingly
                 baseURL = "/apex/MyContactSearchPopup?txt=" + txtId;
                
                 // Following is the id of apex:form control "myForm". 
                 // You need to change that accordingly
                 baseURL = baseURL + "&frm=" + 
                           escapeUTF("{!$Component.myForm}");
                 if (modified == '1') {
                      baseURL = baseURL + searchParam;
                    
                     // START FILTER PARAM IMPLEMENTATION FOR CUSTOM FILTER
                     //var filterVal = '';
                    
                    // determine which account inputfield to get value.
                    //filterVal = '';
                    
                   //Check if we have any filter to add
                   //if(filterVal!=null || filterVal!=''){
                   //     baseURL = baseURL + '&filter=' + 
                   //         escapeUTF("AccountId='"+filterVal+"'");
                   //}
                   // END FILTER PARAM IMPLEMENTATION
               }
                
                 
               // Following is the ID of inputField 
               // that is the lookup to be customized as custom lookup
               if(txtId.indexOf('cName') > -1 ){
                    isCustomLookup = true;
               }
           }
 
           if(isCustomLookup == true){
                openPopup(baseURL, "lookup", 350, 480, "width="+ width + 
                   ",height=480,toolbar=no,status=no,directories=no,
                    menubar=no,resizable=yes,scrollable=no", true);
           }else{
                if(modified == '1') 
                     originalbaseURL = originalbaseURL + originalsearchParam;
                     openPopup(originalbaseURL, "lookup", 350, 480, "width="+ 
                         originalwidth +         
                         ",height=480,toolbar=no,status=no,directories=no,
                         menubar=no,resizable=yes,scrollable=no", true);
          } 
     }
     </script>
     <apex:form >
          <apex:pageBlock title="Contact Details" mode="edit">
               <apex:pageBlockButtons >
                    <apex:commandButton action="{!save}" value="Save"/>
               </apex:pageBlockButtons>
               <apex:pageBlockSection title="Contact Details" columns="1">
                   <apex:inputField value="{!contact.FirstName}"/>
                   <apex:inputField value="{!contact.LastName}"/>
                   <apex:inputField id="cName" value="{!contact.AccountId}"/>
               </apex:pageBlockSection>
          </apex:pageBlock>
     </apex:form>    
</apex:page>

Some improvements that I can see are:
1. Use dynamic component
2. Implement quick create option
3. Optimize parameters on the component.

11 August 2012

You have reached the maximum number of 10 object references on Child


The error "You have reached the maximum number of 10 object references in <Object Name>" refers to the maximum number of unique relationship name you can refer to using formula fields meaning the '__r' on the formula.

For example I created 11 lookup fields on the same object. I named it Parent__c, Parent_2__c ... Parent_11__c and want to reference a field on that object for all lookup fields.

This won't be possible as I can only create up to 10 relationship formula fields for these lookup. Trying to create a formula for Parent_11__r will give an error similar to the below: 



So what's the solution?

If you want to populate the hypothetical field Parent_External_ID_11__c (not displayed above) you need to:
  1. Ask salesforce to increase it.  You can request to increase it up to 15 as of summer 12. See notes from product manager and consequence. http://success.salesforce.com/ideaView?id=08730000000gKsbAAE
  2. Remove unused formula fields if possible.
  3. Convert the field to its appropriate field type (in this case I'm just referencing a text so I need to change the field to Text) and then create an Apex Trigger to populate field on the before or after update event as needed.
Please note that you cannot use workflow rule and field update formula if you are referencing the same relationship because its also counted against the limit.