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.