Wednesday, October 30, 2013

JQuery Tablesorter, Meet PageBlockTable

I am definitely late to the jquery party.  In the last year or so, I've been able to harness jquery to make pages more usable and functional and just finished a poc for another beautiful jquery solution for a recurring visualforce requirement: sortable tables.

A colleague and I were doing a peer review on a visualforce page he had built.  The page had a sortable pageblocktable, which he had enabled with a custom compare function he had built in his controller.  Now, I've seen all kinds of ways of doing sorting in the controller and have done some suboptimal server-side sorting, but I never really thought about trying to keep it client-side.  I figured there'd be a way with javascript but I just didn't have it in me to try to code it up.  So, I poked around the google and sure enough, there's a jquery plugin already built to do it.  And, sure enough, another Salesforce developer, shared her solution using the tablesorter plugin years ago.

So, a few years late to the party :)

Anyway, I wanted to share my variation since I was able to get the tablesorter to work w/ the standard apex component pageblocktable.  Again, with jquery, the solution is basically the following:

1. Import your library as a static resource
2. Reference your resource in your vf page
3. Bind your jquery function and your component

So, applying the parts to my poc, I have a page that looks like this:

*******

<apex:page standardController="Opportunity" tabStyle="Opportunity" extensions="myext" id="thepage">
<apex:includeScript value="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"/>
<apex:includeScript value="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"/>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/ui-lightness/jquery-ui.css" type="text/css" media="all" />
<apex:includeScript value="{!URLFOR($Resource.tablesorter, 'jquery.tablesorter.min.js')}"/>

<script type="text/javascript">
    $j = jQuery.noConflict();    
    $j(document).ready(function () {
    $j("[id$=theaddrs]").tablesorter();

    });

  //some other unrelated js

</script>

<!-- some other visualforce stuff then the heart of the proof of concept: -->

<apex:pageBlock id="theaddrsblock">

                    <apex:pageBlockTable value="{!Addrs}" var="a" id="theaddrs" styleClass="tablesorter" headerClass="header">
                    <apex:column>
                            <apex:facet name="header">
                                <apex:outputText styleClass="header" value="{!$ObjectType.Address__c.Fields.Street__c.Label}" />
                            </apex:facet>
                            <apex:outputText value="{!a.Street__c}" />

                        </apex:column>

<!-- the other columns, closing tags, and that's it -->


******

There is nothing to share about the controller because the sorting is being done w/out a callback to Salesforce.

The sections highlighted in yellow show how little is needed to modify your standard pageblocktable into a sortable table.  Not much, right?

I suggest looking at the documentation or online discussions about optional parameters that can be specified in the tablesorter library but if you just have a few fields in a table that you need to sort, the tool will do a great job of figuring out the data magically.  It's really awesome.

Now, this is only a proof of concept and so there is at least one issue resolve:  the icons to indicate sort direction are displaying on top of the column labels.  Should be able to modify the css to offset the icons.  Worst case, we just remove the styleClass attribute on the outputtext of the column facet.  Anyway, if you don't have to send it back to the controller for some logic or because the dataset is too large, just use the plugin to sort!


Thursday, October 24, 2013

Custom Button to Run Your Entry Criteria before Approval Submission

A sorely lacking feature with Salesforce approvals is the entry criteria rejection message.  If your record does not meet the entry criteria for a given approval process, you get a very generic

Unable to Submit for Approval
This record does not meet the entry criteria or initial submitters of any active approval processes. Please contact your administrator for assistance. 


with no indication of what is missing!  Users hate this.  I mean, HATE this.  They did not configure the approval process and so they've got no idea why they can't submit for an approval.  If the organization is large enough, the sales ops and/or IT help desk gets involved.  This to me is total lunacy and I see it everywhere.  Imagine the productivity you could restore if you provided your users with some meaningful information.  There is an idea you can vote up, if you agree.

There are some options to work around this limitation in the product.  One that I've been playing with is the idea of moving the entry criteria rules into one or many validation rules that only run when a field is set.  For example, you could have a Validated__c flag and create "entry criteria" validation rules to only allow it be set if your entry criteria are met. The flag could then be used by the approval process entry criteria and moves the logic back to the object where it can be surfaced.  Depending on your business, you could use workflows/triggers to uncheck the flag, should something change prior to approval submission.

It was pretty easy to move the entry criteria into validation rules but where I struggled was getting a button to set the field for me and still display the validation rule errors on the standard layout.  I was thinking that a "Validate" button would be more intuitive than setting the flag manually, so here's what I tried:

  1. Button click -> javascript to set field and save 
    • validation rule errors only captured in js but raised through an alert box
  2. Button click -> apex class to set field and save
    • validation rule errors only captured in js, again, raised in alert box
  3. Button click -> url params to set field and auto-save(?!)
    • almost there!

With options 1 and 2 the javascript is only able to surface the errors with an alert.  If your rules are simple, this is probably viable and acceptable for your users.  However, if you have 10 fields that are required for an approval process, your users are probably not going to write down each of the fields they need, then dismiss the alert, then fix the fields and submit.

With option 3, I was looking around to see if there were any non-visualforce options when I came across this article.  The idea was simple: use a custom button to invoke the edit mode for the record and prepopulate the Validate__c flag.  Additionally,  if you add Save=1 to your url, Salesforce could automatically save the record!  It only took a few minutes to configure and everything worked beautifully except the auto-save.  Apparently, Salesforce has disabled the Save parameter, so for now, our users have to click the Validate button, then Save.  Not bad.

The bottom line is that there are options to improve the usability of the entry criteria in your approval process.  You do not have to live with the generic error and you can certainly improve the productivity of your team by moving some of the logic into validation rules.



Friday, October 18, 2013

Draggable and Resizable Modal Popup

One common visualforce solution that I've built for several clients is a modal popup.  At it's core, it is nothing but a hidden outputpanel that is dynamically rendered.  With some styling, you can display the panel "above" your current page, with the current page grayed out.  There are hundreds of blog posts out there covering how to do this: here's one, another, yet another.

These blog posts are incredibly helpful and have inspired me to pass it forward and share the incremental bits that I can.  One thing that I've wanted to do that I just got working in my sandbox was to make these modal popups draggable and/or resizable.  As I've come to learn, jquery makes this ridiculously easy.

If you look at the source code for the draggable example on jquery's site, you'll see that it's 3 parts:
  1. The references to the jquery library
  2. The jquery function
  3. The div that you want to make draggable
Applying this to Salesforce, there are the same 3 parts:

1. A reference to the jquery libraries.  You should probably use static resources, but if you're just testing it out, something like this needs to be on your page:

<apex:includeScript value="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"/>
<apex:includeScript value="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"/>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/ui-lightness/jquery-ui.css" type="text/css" media="all" />

2. Add the jquery script and don't forget the noConflict() requirement.  In my example, I have a div called "pop" that I want to make draggable and resizable.

<script type="text/javascript">
    $j = jQuery.noConflict();
    $j(function() {
    $j( "[id$='pop']" ).draggable().resizable();
});
</script>

3. The div/outputpanel becomes something like this:

<apex:outputPanel id="popupBackground" styleClass="popupBackground" layout="block" rendered="{!displayPopUp}"/>
        <apex:outputPanel id="custPopup"  layout="block" rendered="{!displayPopUp}" >
        <div id="pop" Class="custPopup">
        <!-- your form/pageblock/fields/tables go here-->


Part 3 depends on your styling but it should be pretty easy to apply to your modal popup.  If all goes well you should be able to move your panel around and resize it.  Enjoy!

Tuesday, October 15, 2013

Salesforce formula with CONTAINS

Recently, I learned an interesting detail about the CONTAINS method that can be used in formula fields.  According to the inline formula help, CONTAINS is defined as follows:

CONTAINS(text, compare_text)
Checks if text contains specified characters, and returns TRUE if it does. Otherwise, returns FALSE

Let's say you needed to check for multiple values and for each, set your formula to some other value. The obvious path would be to nest your CONTAINS in a case statement, right?  Something like this, maybe:

CASE(My_Field__c
  CONTAINS(My_Field__c, 'Some Value'), 'New Value'
  CONTAINS(My_Field__c, 'Some Other Value'), 'New Value', etc...)

WRONG!  Turns out, you can't use CONTAINS with CASE, as confirmed here.  Ugh.  So, plan B, might be to use CONTAINS with a nested IF, right?  Yes, it works, but the problem you may run into is that if you are checking for many values, you may hit the maximum size for the formula field, which at the time of writing, is 3900 characters.  

So, when I was researching this, I came across this obscure knowledge article.  What caught my eye was the following:

Example 2:
a. CONTAINS("CA:NV:FL:NY",BillingState)
Will return TRUE if BillingState is CA,NV,V,L,FL:NY or any exact match of "CA:NV:FL:NY".
NOTE: when using contains with the multiple operator (:) contains then becomes equals.

The colon operator allows you to inspect many values, without the overhead of nesting IF-statements. This seems to be very well suited for checking the standard BillingState field, where values will be relatively uniform.  The key difference is the highlighted note indicating the change of function when using the operator.  In the provided example, if you had C, A, N, V, L, F, or Y, it would return true.  But, if you had California, or even CALIFORNIA, it would return false.  By contrast, if you had used a nested-if, you could have introduced some additional flexibility in finding California, CA, Cali, NoCal, SoCal, etc, sacrificing some of your character limit.  So, the takeaway for me is this: if your data is pretty uniform and structured, use the : operator, otherwise use the nested-if. 

Wednesday, October 9, 2013

Multiple Addresses - A Larger Question

One interesting aspect of Salesforce is the address concept.  Since Salesforce is a software (er, no-software) company, they don't really ship anything, right?  Everything is delivered via the cloud.  This perspective seems to have influenced how they model addresses.  Out-of-the-box, addresses are merely attributes of an account and contact in Salesforce.  So, for businesses who actually ship things, like widgets, to other businesses, how does this out-of-box model work?  Say, you are a widget maker and you have big customers, with many locations that consume your widgets.  How should you capture where you're selling your product and where it is sent?

Imagine you have a customer who's organization looks something like this:

  • Joe's Plumbing Worldwide
    • Joe's Plumbing Canada
    • Joe's Plumbing America
      • Joe's Plumbing New England
        • Joe's Plumbing Boston
        • Joe's Plumbing Hartford
      • Joe's Plumbing Chicago
      • Joe's Plumbing Los Angeles
    • Joe's Plumbing Europe
      • Joe's Plumbing France
      • Joe's Plumbing England
If you were selling widgets to Joe's Plumbing, what is important for your business to capture?  Does it only matter that you are selling to "Joe's Plumbing Worldwide"?  Do you have regional team's that support Joe's Plumbing in language?  Do your team's "own" these accounts and selling into them? When you report on sales and service, do you want to measure the selling and servicing at the regional level?  Is your customer data provided or enriched by any 3rd party services?

The account concept is central to Salesforce crm and so the decisions you make around how you model your customers, is significant.  Many appexchange products and services, like address verification, assume that you use the out-of-the-box address fields.  

There are several options to support multiple addresses and each of the options I've listed below can have some variation but the important take-away is that your approach has some implications to think through.

Option 1: Use the native Account hierarchy
Option 2: Create a custom object to hold Addresses
Option 3: Denormalize shipping addresses onto Opportunities/Orders
Option 4: Add additional shipping address fields onto your Account object

For example, if you go with option 1, do your shipping records mean anything?  Should your teams own these records?  Do you need to restrict the ability to create opportunities to just the parent account? Do you want to restrict the ability to create contacts or tasks to just the parent account?  If not, is your reporting ready for a hierarchy of data to roll up?

If you like option 2, do you need to report on shipping information?  Does your address data need to be verified or enhanced by a 3rd party and if so, does that 3rd party support your custom address object?

As is usually the case, there are many ways to solve the problem.  It's a matter of figuring out what is the best fit for your business and thinking through the implications of that decision.


Thursday, October 3, 2013

Salesforce Quotes


There is no doubt that the Salesforce Quote functionality is useful.  But could this object be any more specialized?  We've run into so many issues with customizations.  Here's a list of some issues or limitations I've recently come across:

1. You cannot override the standard Quote view with a custom visualforce page.  The option to override View is not available.
  • I suspect that because the object is so specialized (oppty sync, PDF creation, etc) that this is not likely to change.  If you need custom quote functionality, you may need to build your own from the object up.

2. The standard Discount field is not available as a merge field in Email templates.
  • As a workaround, you can create a formula field that references the out-of-box field and then use the formula field in your email template.

3. You cannot roll up list price from the line item.
  • Apparently, list price is a reference to the PricebookEntry object and so it is not a field that can be summarized.  To proceed, you need to create another currency field and populate it with a workflow on the QLI based on your business requirements.

4. You can only control edit access to Sales Price from a Profile-level parameter.
  • It's not so bad to have to use this parameter, since you can also put it in a permission set, but it's pretty inconsistent from the way the rest of the profile/page layouts behave.

5. If your record is locked as the outcome an approval process, you will not be able to save your pdf to the quote using the Create PDF button.
  • Speaking of inconsistent... on all other objects that are locked, you can almost always add an attachment.  To get around this, I changed our approvals to unlock the record, then used validation rules to keep the QLI from being changed.


The Create PDF button on Quote

I was asked about the ability to restrict the creation of a PDF for a given quote until a couple business rules had been met.  As I thought about the solution, a couple ideas came to mind:

1. Add a record type for the ok Quotes and assign that record type a new layout that includes the Create PDF button.  Remove the Create PDF from the other page layout.
2. Modify the behavior of the existing Create PDF button to check the status first.
3. Create a new VF page to replicate the Create PDF functionality.

Given the timing and other project considerations, I opted for #2, if it was feasible.  I knew that #1 would work but I was worried about introducing a record type and then having to roll it back when another quote-related project went live.

To start, I had to figure out if I could see the code that the out-of-box button was calling to render the PDF.  With chrome it was pretty easy to inspect the element and see the code the button was calling.

With a minor bit of additional javascript, the quote's status can be interrogated first and an informational alert raised, if certain business criteria are met:

/*********************************************************************
if('{!Quote.Status}'!= 'XYZ' ) 
{
// do some business logic...
var isOk = true;  
} if(isOk) 

var pdfOverlay = QuotePDFPreview.quotePDFObjs['quotePDFOverlay']; 

pdfOverlay.dialog.buttonContents = "<input value=\'Save to Quote\' class=\'btn\' name=\'save\' onclick=\"QuotePDFPreview.getQuotePDFObject(\'quotePDFOverlay\').savePDF(\'0\',\'0\');\" title=\'Save to Quote\' type=\'button\' ><input value='Save and Email Quote' class='btn' name='saveAndEmail' onclick=\"QuotePDFPreview.getQuotePDFObject(\'quotePDFOverlay\').savePDF(\'1\');\"; title='Save and Email Quote' type='button' ><input value=\'Cancel\' class=\'btn\' name=\'cancel\' onclick=\"QuotePDFPreview.getQuotePDFObject(\'quotePDFOverlay\').close();\" title=\'Cancel\' type=\'button\' >"; 

//change this to use the correct template for your business/environment!! 
pdfOverlay.summlid = 'XXXXXXXXXXXXX'; 

pdfOverlay.setSavable(true); 

//change this to use the quote id 
pdfOverlay.setContents('/quote/quoteTemplateDataViewer.apexp?id={!Quote.Id}','quote/quoteTemplateHeaderData.apexp?id={!Quote.Id}'); 

pdfOverlay.display(); 

else 

//raise an alert to let the user know about some business rule
alert('The Quote requires XYZ before the PDF can be generated.'); 
}

*************************************************************/