Friday, August 9, 2013

Auto save with TinyMCE and Salesforce

I'm on a project where our users have large numbers of long text areas and are often working on old-ish computers.  One common complaint was that they'd spend a good amount of time typing something only to find that they'd lost it when they clicked Save.  We explored some options with some features within HTML 5, like local storage, but settled on trying auto-save.  Auto save is a common feature in most online services like blogger or gmail, so this seemed like a good model to follow.  Additionally, it seemed less browser dependent, which is also good.

The system is composed of a Visualforce page using TinyMCE as the rich text editor.  TinyMCE has a (somewhat buggy) function that will tell you if an editor instance "is dirty".  We set a timer to check this attribute every few seconds.  If the attribute returns true, we can execute a controller method via javascript remoting.

Putting it all together:

A visual indicator (to show the user that the auto save ran):

<div id="saving" class="minitext" style="color:#666; font-style:italic; display: none">AutoSaving...</div>

The timer:
var $j = jQuery.noConflict();  
// First we tell this to run when the page is loaded
$j(document).ready(function()
{
  $j(function()
  {
    setInterval("auto_save('{!$Component.theform.thepageblock.thepbs.somefield}')",5000);
  });

});


The auto_save function referenced in the timer:
function auto_save(inputs)
{
  // First we check if any changes have been made to the editor window
  if(tinyMCE.getInstanceById(inputs).isDirty())
  {
    $j('#saving').show();
    var content = tinyMCE.get(inputs);
    var notDirty = tinyMCE.get(inputs);
    saveTestField('Some_Field__c', content.getContent());

    notDirty.isNotDirty = true;
   }
else
  {
    $j('#saving').hide();
    return false;
  }


The saveTestField function:
function saveField(fieldName, fieldValue) 
{
    Visualforce.remoting.Manager.invokeAction('{!$RemoteAction.AutoSave.updateFieldValue}', recId, fieldName, fieldValue, 
        function(result, event){
            if (event.status) {
            } else if (event.type === 'exception') {
                console.log(event.message);
            }
        }, 
        {escape: true}
    );


The Apex method:
@RemoteAction
public static Boolean updateFieldValue(Id recordId, String fieldName, Object fieldValue)
{
String sObjectName = recordId.getSObjectType().getDescribe().getName();
String queryString = 'Select Id, ' + String.escapeSingleQuotes(fieldName) + ' From ' + String.escapeSingleQuotes(sObjectName) + ' Where Id = \'' + String.escapeSingleQuotes(recordId) + '\'';
Sobject record = Database.query(queryString);

record.put(fieldName, fieldValue);
update record;

return true;

}


Observations:

  • UI validations are completely ignored.  If you have a required field in your UI, the field will still commit, without error.
  • The auto save only commits the field passed to the controller.  No other field is saved.

Issues

  • One issue that I observed while assembling this was that the isDirty attribute wasn't always reliable.  It turns out that you have to tell TinyMCE to save at some point for the attribute to reset itself.  You can do it by adding this line: tinyMCE.get(inputs).save();
  • Another issue: this recipe works well for existing records (records with Id).  If you're inserting a new record, you'll need to modify accordingly.

Credits
This was assembled with some helpful tips and tricks from other folks sharing their solutions.  Thank you for sharing!
https://github.com/pbattisson/Visualforce-Autosave/
http://webnv.net/2008/02/10/autosaving-with-jquery-and-tinymce/

Opportunity Contact Role "Trigger"

Among the many limitations of the standard OpportunityContactRole object in Salesforce is the inability to put a trigger on it.  The use case I wanted to solve was to aggregate and copy the contact information related to certain roles to the parent opportunity.  Some options I considered:

1. Create a batch job to query OCR nightly and update the parent opportunity
2. Create a trigger on Opportunity updates to fetch the OCR data
3. A combination of 1 and 2
4. Replace the standard OCR with a custom OCR and create the necessary trigger

I finally came across this blog post, and with some tinkering, found that it worked for our use-case. The idea is to hide a inline visualforce page within the standard layout and use that vf page's action property to run some code.  The author chose an asynchronous opportunity update whereas I went with a synchronous update.  The action property on the page tag is generally used for redirects, but could certainly be used for defaulting data as well.

Some issues I came across:

1. Since visualforce is served up from a different domain, I was seeing my page redirected by a servlet to a "page not found".  I was able to stop this by returning a non null page reference.
2. The js console in chrome logs some refusal errors due to the different domains.  This went away with the resolution to #1.
3. I was unable to refresh the parent page from the inline vf page after the update, so the user has to manually refresh the opportunity to see the effect.  This is still the case but our users were ok with it.
4. The hidden vf page's controller is trying to execute a DML.  This is usually followed by a page refresh.  The problem is that the page refresh is occurring within the inline VF, which,when it renders, again executes the controller's action.  This can cause some serious looping, which is why it is imperative for the action method to check whether an update is required or not.


Thursday, August 1, 2013

Visualforce Page with ContentType in MultiByte Language

I'm working on a project where there are several multibyte languages, like Chinese and Arabic, that are supported.  One issue that we discovered in a new feature is related to exporting some content as a Word file.  The issue was in specifying the filename.  As you probably know, the syntax for something like this is:


<apex:page Controller="yourController" contenttype="application/msword#yourfilename" .. />

This works great but if you are substituting yourfilename with something from the controller, one symptom you could see is the file generated with the name of your VF page.  So, let's say you were doing something like this:

<apex:page Controller="yourController" contenttype="application/msword#{!someVar}" .. />

If {!someVar} is a value that is in Chinese or Arabic, your file name will probably look like "YourVFPage.doc" instead of "{!someVar}.doc".

There wasn't much in the Salesforce support community so I've come up with a workaround:

1. I added a charset identifier to the contenttype attribute like so:

<apex:page Controller="yourController" contenttype="application/msword#{!someVar};charset=utf-8" .. />

When you try to view your file, you get a little closer - your filename will likely look like: "------.doc".

2. The following thread, gave me the idea for the fix for ---- characters.  In the controller, we just encode the someVar value like so:

  String someVar = EncodingUtil.urlEncode(myString, 'UTF-8'); 
  return someVar;

When you try to view your file, you'll see the multibyte value in the filename.  You may have to do some substitution to remove any other characters, but this should get you closer to a user acceptable solution.

Tuesday, June 18, 2013

MultiSelect and JQuery/Javascript

I had a requirement to give users a way to quickly select a couple of values in a multi-select picklist based on some other value they had previously selected on the form.  In our case, if the selected Language was X, then pick Region 1, Region 3, and Region 4 from the multi-select picklist automatically.

This one took a while to piece together so hopefully this will help someone out.

On my VF page, I created an anchor tag to act like a "Command Link":


  <a href="javascript:void(0);" id="selectAllRegions">[Select All Regions for Language]</a></span>

Using some jquery, I catch the click as follows:

  j('#selectAllRegions').click(function () {
        selectRegions('{!$Component.regionsMS}');

        });

The regionsMS id is the id of your multiselect apex:inputfield.

The "selectRegions" function does the following:

function selectRegions(objId){
    var multiSelect;
    var unSelectedId = objId + "_unselected";
    var selectedId = objId + "_selected";
   
    multiSelect = document.getElementById(unSelectedId);
        for (i = 0; i < multiSelect.options.length; i++) {
            if(multiSelect.options[i].text == "Region 1"){
            multiSelect.options[i].selected = true;}
        }

        javascript:MultiSelectPicklist.handleMSPSelect(objId);
}

The key to making this work was this last function, which came up in a search here (there's some other NSFW stuff there, just fyi).  If you use firebug or chrome's developer tools, you'll see how the script interacts with the elements that make up the multiselect control.

And there you have it - when you click the link "Select All Regions for Language", Region 1 is selected.  All that needs to be done now, is evaluate the selected language and then change which region values are selected.

Friday, June 7, 2013

Rich Text Editors

On a recent project, I was asked to implement a visualforce page with rich text editing capability.  As I've come to learn, when you put an <apex:inputTextArea> tag inside a visualforce page, you don't get a rich text editor.  You get a text area.  No toolbars for formatting, unlike the standard layouts.  The standard layout editor appears to be ckeditor but if you're using visualforce, you have to put in the rich text editor yourself.  Fortunately, there are lots of editors out there.  I had no idea.

Based on the project needs and features, I ended up implementing the following for user evaluation:

  1. TinyMCE
  2. CKEditor
  3. NicEdit
  4. Redactor

TinyMCE ended up being the selected editor, mostly because of the ICE plugin.  I use strikingly to power my website and strikingly uses TinyMCE as it's editor.  It's fairly straightforward to implement:


  1. Download TinyMCE and upload the zip it as a static resource
    1. If you're using ICE, you'll need to include it in your plugin directory, rezip, and upload
  2. Update you visualforce page as follows
<apex:includeScript value="{!URLFOR($Resource.tinymce, 'tinymce/jscripts/tiny_mce/tiny_mce.js')}"/> 
...

<apex:inputTextArea value="{!TEST__c.Some_Field__c}" id="somefield" style="width:100%;" styleclass="mceEditor"/>
<!--this is the initialization required for TINYMCE  -->
                <script type="text/javascript">
                   tinymce.init({
                            mode : "textareas",
                            editor_selector :"mceEditor",
                            theme : "advanced",
                            plugins : "ice,icesearchreplace,spellchecker,pagebreak,style,layer,table,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template,visualchars,wordcount",
                            theme_advanced_buttons1: 'ice_togglechanges,ice_toggleshowchanges,iceacceptall,icerejectall,iceaccept,icereject,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect',
                            theme_advanced_buttons2: 'spellchecker,cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,forecolor,backcolor',
                            theme_advanced_buttons3: 'tablecontrols,wordcount',
                            theme_advanced_buttons4: "",
                            theme_advanced_toolbar_location: "top",
                            theme_advanced_toolbar_align: "left",
                            theme_advanced_toolbar_location : "top",
                            theme_advanced_statusbar_location : "bottom",
                            theme_advanced_resizing : true,
                            ice: {
                                          user: { name: '{!$User.Alias}', id: '{!$User.Alias}'},
                                          preserveOnPaste: 'p,a[href],i,em,strong'
                                },
                            width: "100%",
                            height: "200"      
                        });
                </script>

I highlighted a couple sections that are noteworthy:
  • You need to mention that you're using TinyMCE so you'll need the <apex: includeScript> tag
  • You can have multiple TextArea fields on your page and selectively enable TinyMCE using the editor_selector property when you initialize the editor.  Just set the styleClass property on your text area fields that you want to override with TinyMCE.
  • If you're using ICE, the modification here allows you to capture the user who edits the text.
And if all goes well, you should have something like this:

Deployment Failure

I recently assisted an organization with developing a couple of triggers to help them roll up some child data onto the parent records.  On the evening we were set to deploy, we ran into a couple issues, both of which could be filed under "WTF":

1. Change Sets were disabled for the organization
2. Deploying via Eclipse generated over 150 errors in the managed packages that were installed in their org

The client's system admin cases opened for both issues - the first with Salesforce, the second with the managed package vendor.

The response from the managed package vendor was illuminating so I wanted to share.  They forwarded us this community thread in which the question of managed package errors was settled:

http://boards.developerforce.com/t5/Apex-Code-Development/Unit-Test-Code-Coverage-and-Managed-Packages/m-p/471121#M86324

In summary: if you deploy with change sets, managed package code is ignored.  If you deploy with Eclipse, you're out of luck if there are test class failures in the managed package.  

Will update the blog with Salesforce's explanation of issue #1.  

Tuesday, May 21, 2013

User Hierarchy based Sharing

I have a client who has flattened out their role hierarchy to enable some other business processes within Salesforce.  So when it came for them to implement an object in Salesforce that required some hierarchy based sharing we had to build something custom based on the user record's manager hierarchy.  For example, if Joe owns a record, his manager, Jane, should have read access to the record.  Jane's manager Mike, should also have read access.  This access should continue to the top of the hierarchy.

In the code below, keep this in mind:

Joe -> Jane -> Mike -> Mary

In our org, the sensitive records that require Private org wide defaults give record owner's read-write access by virtue of their ownership of the record.  For all others to have read access, we have to create sharing records for the object.

A share is composed of the following elements:
  • ParentId - the record that you want to share with others
  • RowCause - the custom reason you are sharing this record
  • AccessLevel - the level of access being given (in our ex: read)
  • UserOrGroupId - the user (or group) who should have access

By creating a record with these required elements, these private records can be shared with those users (or groups) mentioned in the shares.

Programmatically, it is pretty easy to create a trigger on an object and create a related share for the object's owner.  Salesforce's documentation of apex managed sharing does an adequate job of this.  Where I scratched my head a little was figuring out how to share all the way up the hierarchy.  And do the sharing without hitting any governor limits.  I did some searching of blogs and found an interesting post by Jeff Douglas.  Jeff's solution is elegant, but I wanted to try another way myself.  In plain english here is what I wanted to do:

1. Create an object share record for the record's owner's manager
2. Then, create another object share for that manager's manager
3. and so on...
4. Insert list of object share records

It felt like a loop to me, so what I tried initially was to iterate through the object share collection, and for each record in the collection, create another share record, assign the manager, then add it to the collection and continue until a manager is no longer found for the user.  In this snippet, the collection of all users and their managers is held in a map:

         for(User u: [Select u.Id,u.ManagerId from User u])
        {
            mapUserManagers.put(u.Id,u.ManagerId);
        }

for(someObject__Share os: sharesToCreate)
        {
            if(mapUserManagers.get(os.UserOrGroupId) != null)
            {
                someObject__Share os_mgr = new someObject__Share ();
                os_mgr.AccessLevel = MGR_ACCESS;
                os_mgr.ParentId = es_mgr.ParentId;
                os_mgr.RowCause = MGR_ROW_CAUSE;
                os_mgr.UserOrGroupId = mapUserManagers.get(os_mgr.UserOrGroupId);
                sharesToCreate.add(os_mgr);
            }
        }

This approach throws an error:  "Cannot Modify a Collection While It Is Being Iterated"

Some folks over at stackexchange explain the issue with the index behind this loop.

So, I took a slightly different approach to the loop:

        allUsers = [Select u.Id,u.ManagerId from User u];
        for(User u: allUsers)
        {
            mapUserManagers.put(u.Id,u.ManagerId);
        }
        Id managerId;      
        for(SomeObject__c so: RecsToProcess)
        {
            managerId = mapUserManagers.get(so.OwnerId);
            do{
                SomeObject__Share sos = new SomeObject__Share ();
                sos.AccessLevel = SHARE_MGR_ACCESS;
                sos.ParentId = so.Id;
                sos.RowCause = SHARE_MGR_ROW_CAUSE;
                sos.UserOrGroupId = managerId;
                sharesToCreate.add(sos);
                managerId = mapUserManagers.get(managerId);
            } while (managerId !=null);
        }
             
        //allow for partial successes
        Database.SaveResult[] srList = Database.insert(sharesToCreate, false);

This compiles without issue and achieves the effect of creating a collection of share records for each of the managers with only 1 SOQL call.  It's another way to solve the hierarchy loop challenge.  Hope it helps you find a solution to your own problem.