Paris 13-12-2015

Peace For ParisMy girlfriend and I were lucky to escape from Le Bataclan on Friday night in Paris while watching The Eagles of Death Metal.

We survived thanks to the love and compassion of the people of Paris and we thank them from the bottom of our hearts and stand by them, united, in this time of tragedy.

Our thoughts are very much with those that were not as lucky as we were and our love goes out to them and their families.

#LoveNotHate


You can read an interview we did with Louder Than War magazine here along with our return to Paris following the bands return here.


 

Advertisements
Posted in travel, Uncategorized

Salesforce Lightning Lookup Component : Version 2

Lightning Yellow Pages

Version 2

This is Version 2 of the Salesforce Lightning Lookup Component. I had one of those, ‘Why didn’t I do it like that instead?‘ moments in the shower this morning. Essentially, it’s less code as I’ve done away with the rendering of the inner list components and plumped straight for throwing the Apex Controller results into an attribute and using an <aura:iteration> to render them. The original version is here for reference.

Note: There appears to be a bug with using <aura:if> in the AccountLookup component. If you experience difficulty with this, a workaround can be found here

On with the show …

I’ve been playing around with Salesforce Lightning components and one of the obvious things that seems to be missing is a UI component to lookup a record in a Salesforce custom object and return its Id.

This was really simple in Visualforce. You could just use an <apex:inputField> tag and bind it to an objects lookup field in your controller. Salesforce would take care of the rest. Lightning, however, is a different beast and centres around building client side apps or components in Javascript and utilising various API’s for dealing with any server side requests.

There’s quite a bit of code here so I’ve added it all to a github repository. (Links at the bottom)

Design

There were a few things to consider:

  • It should work both within a desktop app and within the Salesforce Lightning mobile app.
  • It should be intuitive and have the look and feel of a lightning app.
  • It should be reusable, easily configured and plumbed into another component or app.

Cake

As I said, there’s quite a bit of code so before we begin, like all good cake recipes, let’s look at the finished result (6Mb Animated GIF file, please be patient while it loads):

Ligntning Lookup Component

Ligntning Lookup Component

Look & Feel (using the Salesforce Lightning Design System)

The look and feel comes from the Salesforce Lightning Design System or SLDS for short. This is a prerequisite, so you’ll need to install this before you press ahead and load any of the code into your org. I’m using SLDS092 but you should be able to a later version if you wish. You can install it into your org using the links here. This library provides a whole plethora of CSS, images to make your app and components look like the real deal. It also covers the UI design and how to structure your markup. For the lookup component I’ve used the markup described in the Lookups component of the SLDS. In some cases I’ve replaced the standard HTML tags with lightning framework markup such as <ui:inputText> instead of <input>. I’ve also had to create an <c:svg> component to use instead of the <svg> tag which is not supported in lightning. This is described in the Trailhead module: Build a Lightning App with the Lightning Design System.

Show me the code!

Lightning component bundle: svg

First things first, I need to show you the svg component. You need this so you can render the icons as nice scalable graphics without using the <svg> tag. Now this is pretty much lifted straight from the trailhead module so I’m not going to bore you with how it works. You can check out the trailhead yourself.

Here’s the component markup:

Component

<aura:component >
  <aura:attribute name="class" type="String" description="CSS classname for the SVG element" />
  <aura:attribute name="xlinkHref" type="String" description="SLDS icon path. Ex: /assets/icons/utility-sprite/svg/symbols.svg#download" />
  <aura:attribute name="ariaHidden" type="String" default="true" description="aria-hidden true or false. defaults to true" />
</aura:component>

It has a renderer defined which looks like this:

Renderer

({
  render: function(component, helper) {
    //grab attributes from the component markup
    var classname = component.get("v.class");
    var xlinkhref = component.get("v.xlinkHref");
    var ariaHidden = component.get("v.ariaHidden");

    //return an svg element w/ the attributes
    var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute('class', classname);
    svg.setAttribute('aria-hidden', ariaHidden);
    svg.innerHTML = '<use xlink:href="'+xlinkhref+'"></use>';
    return svg;
  }
})

Apex Controller: LookupSObjectController

We’re going to need an apex controller on the server side to query the database and return the matching records. To do this create the following aura enabled Apex class:

Apex class

/**
 * Apex Controller for looking up an SObject via SOSL
 */
public with sharing class LookupSObjectController 
{
    /**
     * Aura enabled method to search a specified SObject for a specific string
     */
    @AuraEnabled
    public static Result[] lookup(String searchString, String sObjectAPIName)
    {
        // Sanitze the input
        String sanitizedSearchString = String.escapeSingleQuotes(searchString);
        String sanitizedSObjectAPIName = String.escapeSingleQuotes(sObjectAPIName);

        List<Result> results = new List<Result>();

        // Build our SOSL query
        String searchQuery = 'FIND \'' + sanitizedSearchString + '*\' IN ALL FIELDS RETURNING ' + sanitizedSObjectAPIName + '(id,name) Limit 50'; 

        // Execute the Query
        List<List<SObject>> searchList = search.query(searchQuery);

        // Create a list of matches to return
        for (SObject so : searchList[0])
        {
            results.add(new Result((String)so.get('Name'), so.Id));
        }
        
        return results;
    }
    
    /**
     * Inner class to wrap up an SObject Label and its Id
     */
    public class Result
    {
        @AuraEnabled public String SObjectLabel {get; set;}
        @AuraEnabled public Id SObjectId {get; set;}
        
        public Result(String sObjectLabel, Id sObjectId)
        {
            this.SObjectLabel = sObjectLabel;
            this.SObjectId = sObjectId;
        }
    }
}

Notice the @AuraEnabled keyword. This allows the method to be called from the lightning framework. The lookup method constructs a SOSL query using the passed in search string and the objects API name. It returns a list of Results that contain a label and the Id of the matching SObjects.

Lightning component bundle: LookupSObject

On the client side we have a reusable Lightning component. The component contains attributes to specify the SObjects API Name, the label and plural label to apply along with an optional SVG image path and class from the SLDS. If the latter aren’t specified it uses a default icon, in this case a star.

There are two events registered: updateLookupIdEvent and clearLookupIdEvent. The former is fired when the user chooses an entry from the list, the latter is fired when the user clears a selection (via the cross icon.)

The component uses a keyup event to pass the user entered search string to a controller method. This in turn calls the server side Aura Enabled method.

The markup generated adheres to the SLDS Lookup component design. We’re going to use an <aura:iteration> to bind to the results of the Apex controller search. The matches attribute is where the results will get stored. You’ll notices inside the iteration is <a> element which uses the SObjects Id within the rendered element id so that we can easily capture it within the controller when the item is selected.

The component markup looks like this:

Component

<aura:component controller="LookupSObjectController" >
    <!-- Required Scripts/Styles -->
    <!-- Salesforce Lightning Design System : https://www.lightningdesignsystem.com/ -->
    <ltng:require styles="/resource/SLDS092/assets/styles/salesforce-lightning-design-system-ltng.css" />
    
    <!-- Attributes -->
    <aura:attribute name="sObjectAPIName" type="String" required="true" description="The API name of the SObject to search" />
    <aura:attribute name="label" type="String" required="true" description="The label to assign to the lookup, eg: Account" />
    <aura:attribute name="pluralLabel" type="String" required="true" description="The plural label to assign to the lookup, eg: Accounts" />
    <aura:attribute name="listIconSVGPath" type="String" default="/resource/SLDS092/assets/icons/custom-sprite/svg/symbols.svg#custom11" description="The static resource path to the svg icon to use." />
    <aura:attribute name="listIconClass" type="String" default="slds-icon-custom-11" description="The SLDS class to use for the icon." />
    <aura:attribute name="searchString" type="String" description="The search string to find." />
    <aura:attribute name="matches" type="LookupSObjectController.Result[]" description="The resulting matches returned by the Apex controller." />

    <!-- Events -->
    <aura:registerEvent name="updateLookupIdEvent" type="c:UpdateLookupId"/>    
    <aura:registerEvent name="clearLookupIdEvent" type="c:ClearLookupId"/>    
    
    <!-- Lookup Markup : See https://www.lightningdesignsystem.com/components/lookups -->
    <div class="slds"> 
    <div aura:id="lookup-div" class="slds-lookup" data-select="single" data-scope="single" data-typeahead="true">
        <!-- This is the Input form markup -->
        <div class="slds-form-element">
            <label class="slds-form-element__label" for="lookup">{!v.label}</label>
            <div class="slds-form-element__control slds-input-has-icon slds-input-has-icon--right">
                <c:svg class="slds-input__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                <!-- This markup is for when an item is currently selected -->
                <div aura:id="lookup-pill" class="slds-pill-container slds-hide">
                        <span class="slds-pill slds-pill--bare">
                            <span class="slds-pill__label">
                                <c:svg class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!v.searchString}
                            </span>
                            <button class="slds-button slds-button--icon-bare" onclick="{!c.clear}">
                                <c:svg class="slds-button__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#close" />
                                <span class="slds-assistive-text">Remove</span>
                            </button>
                        </span>
                    </div>
                    <!-- This markup is for when searching for a string -->
                    <ui:inputText aura:id="lookup" value="{!v.searchString}" class="slds-input" updateOn="keyup" keyup="{!c.search}" />
                </div>
            </div>
            <!-- This is the lookup list markup. Initially it's hidden -->
            <div aura:id="lookuplist" class="slds-lookup__menu slds-hide" role="listbox">
                <div class="slds-lookup__item">
                    <button class="slds-button">
                        <c:svg class="slds-icon slds-icon-text-default slds-icon--small" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                        &quot;{!v.searchString}&quot; in {!v.pluralLabel}
                    </button>
                </div>
                <ul aura:id="lookuplist-items" class="slds-lookup__list" role="presentation">
                    <aura:iteration items="{!v.matches}" var="match">
                        <li class="slds-lookup__item">
                            <a id="{!globalId + '_id_' + match.SObjectId}" role="option" onclick="{!c.select}">
                                <c:svg class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!match.SObjectLabel}
                            </a>
                        </li>
                    </aura:iteration>
                </ul>
            </div>
        </div>
    </div>
</aura:component>

There is an associated controller to handle the events. The search method, as mentioned earlier, is called from the keyup event on the search. The select method is called when the user chooses a result from the rendered list. The clear method is called when the user clears the selected item by clicking the cross icon:

Controller

({
    /**
     * Search an SObject for a match
     */
    search : function(cmp, event, helper) {
        helper.doSearch(cmp);        
    },

    /**
     * Select an SObject from a list
     */
    select: function(cmp, event, helper) {
        helper.handleSelection(cmp, event);
    },
    
    /**
     * Clear the currently selected SObject
     */
    clear: function(cmp, event, helper) {
        helper.clearSelection(cmp);    
    }
})

You’ll notice that each of the methods delegate to a helper. The helper does all the grunt work and looks like this:

Helper

({
    /**
     * Perform the SObject search via an Apex Controller
     */
    doSearch : function(cmp) {
        // Get the search string, input element and the selection container
        var searchString = cmp.get('v.searchString');
        var inputElement = cmp.find('lookup');
        var lookupList = cmp.find('lookuplist');

        // Clear any errors and destroy the old lookup items container
        inputElement.set('v.errors', null);
        
        // We need at least 2 characters for an effective search
        if (typeof searchString === 'undefined' || searchString.length < 2)
        {
            // Hide the lookuplist
            $A.util.addClass(lookupList, 'slds-hide');
            return;
        }

        // Show the lookuplist
        $A.util.removeClass(lookupList, 'slds-hide');

        // Get the API Name
        var sObjectAPIName = cmp.get('v.sObjectAPIName');

        // Create an Apex action
        var action = cmp.get('c.lookup');

        // Mark the action as abortable, this is to prevent multiple events from the keyup executing
        action.setAbortable();

        // Set the parameters
        action.setParams({ "searchString" : searchString, "sObjectAPIName" : sObjectAPIName});
                          
        // Define the callback
        action.setCallback(this, function(response) {
            var state = response.getState();

            // Callback succeeded
            if (cmp.isValid() && state === "SUCCESS")
            {
                // Get the search matches
                var matches = response.getReturnValue();

                // If we have no matches, return nothing
                if (matches.length == 0)
                {
                    cmp.set('v.matches', null);
                    return;
                }
                
                // Store the results
                cmp.set('v.matches', matches);
            }
            else if (state === "ERROR") // Handle any error by reporting it
            {
                var errors = response.getError();
                
                if (errors) 
                {
                    if (errors[0] && errors[0].message) 
                    {
                        this.displayToast('Error', errors[0].message);
                    }
                }
                else
                {
                    this.displayToast('Error', 'Unknown error.');
                }
            }
        });
        
        // Enqueue the action                  
        $A.enqueueAction(action);                
    },

    /**
     * Handle the Selection of an Item
     */
    handleSelection : function(cmp, event) {
        // Resolve the Object Id from the events Element Id (this will be the <a> tag)
        var objectId = this.resolveId(event.currentTarget.id);

        // The Object label is the inner text)
        var objectLabel = event.currentTarget.innerText;

        // Log the Object Id and Label to the console
        console.log('objectId=' + objectId);
        console.log('objectLabel=' + objectLabel);
                
        // Create the UpdateLookupId event
        var updateEvent = cmp.getEvent("updateLookupIdEvent");
        
        // Populate the event with the selected Object Id
        updateEvent.setParams({
            "sObjectId" : objectId
        });

        // Fire the event
        updateEvent.fire();

        // Update the Searchstring with the Label
        cmp.set("v.searchString", objectLabel);

        // Hide the Lookup List
        var lookupList = cmp.find("lookuplist");
        $A.util.addClass(lookupList, 'slds-hide');

        // Hide the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.addClass(inputElement, 'slds-hide');

        // Show the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.removeClass(lookupPill, 'slds-hide');

        // Lookup Div has selection
        var inputElement = cmp.find('lookup-div');
        $A.util.addClass(inputElement, 'slds-has-selection');

    },

    /**
     * Clear the Selection
     */
    clearSelection : function(cmp) {
        // Create the ClearLookupId event
        var clearEvent = cmp.getEvent("clearLookupIdEvent");
        
        // Fire the event
        clearEvent.fire();

        // Clear the Searchstring
        cmp.set("v.searchString", '');

        // Hide the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.addClass(lookupPill, 'slds-hide');

        // Show the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.removeClass(inputElement, 'slds-hide');

        // Lookup Div has no selection
        var inputElement = cmp.find('lookup-div');
        $A.util.removeClass(inputElement, 'slds-has-selection');
    },

    /**
     * Resolve the Object Id from the Element Id by splitting the id at the _
     */
    resolveId : function(elmId)
    {
        var i = elmId.lastIndexOf('_');
        return elmId.substr(i+1);
    },

    /**
     * Display a message
     */
    displayToast : function (title, message) 
    {
        var toast = $A.get("e.force:showToast");

        // For lightning1 show the toast
        if (toast)
        {
            //fire the toast event in Salesforce1
            toast.setParams({
                "title": title,
                "message": message
            });

            toast.fire();
        }
        else // otherwise throw an alert
        {
            alert(title + ': ' + message);
        }
    }
})

The doSearch method takes care of calling the Apex controller and storing the results in the matches attribute.

The handleSelection method takes the selected entry and fires the updateLookupIdEvent before modifying the CSS classes to show the Lookup in its selected state.

The clearSelection method fires the clearLookupIdEvent before modifying the CSS classes to revert back to the search state.

The resolveId method splits the SObject Id from the end of the element id. This is used by the handleSelection method to obtain the SObjects Id. Remember, we hashed it in to the <a> elements id.

The displayToast is simply an error logging method that renders a toast popup in the Salesforce1 app or an alert in the browser.

Lightning event: UpdateLookupId

This is the event fired when the user selects an item from search results. It has a single attribute that gets set with the Id of the selected record. You’ll notice this is a component event rather than an application event. Be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Update Lookup Id" >
    <aura:attribute name="sObjectId" type="Id" required="true" description="The Id of the selected SObject." />
</aura:event>

Lightning event: ClearLookupId

This is the event fired when the user clears the selected item. It has no attributes. Again, you’ll notice this is a component event rather than an application event. Once again, be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Clear the Lookup" />

Ok, great. Now can I see how you use it?

We’re going to wrap this up with an example Account Lookup component which you can plug straight into Salesforce1 via a Component Tab. Since the component implements flexipage:availableForAllPageTypes you could plug it straight into Lightning Builder if you wished. If you wanted to embed it in a Visualforce page you could do that as well. I won’t go into how to do that here but you’ll find details in the Lightning developer documentation.

Lightning component: AccountLookup

First lets look at the component markup. There’s an attribute to hold the Record Id. This is hooked up to the force:recordView component which is only rendered if an Id is present. We have two event handlers listening for the UpdateLookupId and ClearLookupId events. Note the names tie up to the registered events in the LookupSObject component. These event handlers hook up to the components controller via the handleAccountIdUpdate and handleAccountIdClear methods. The LookupSObject component markup takes the labels and icon resources as attributes.

Component

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes">
    <!-- Attributes -->
    <aura:attribute name="recordId" type="Id" description="The current record Id to display" />
    
    <!-- Event handlers -->
    <aura:handler name="updateLookupIdEvent" event="c:UpdateLookupId" action="{!c.handleAccountIdUpdate}"/>
    <aura:handler name="clearLookupIdEvent" event="c:ClearLookupId" action="{!c.handleAccountIdClear}"/>
    
    <!-- Lookup component -->
    <c:LookupSObject label="Account" pluralLabel="Accounts" sObjectAPIName="Account" listIconSVGPath="/resource/SLDS092/assets/icons/standard-sprite/svg/symbols.svg#account" listIconClass="slds-icon-standard-account" />

    <!-- Record view -->
    <aura:if isTrue="{!v.recordId!=null}">
        <force:recordView recordId="{!v.recordId}" />
    </aura:if>
</aura:component>

Now lets take a look at the controller. You’ll see the handleAccountIdUpdate method gets the Id from the Event and updates the recordId attribute. The handleAccountIdClear simply nulls the recordId attribute. And that’s it, simples.

Controller

({
    /**
     * Handler for receiving the updateLookupIdEvent event
     */
    handleAccountIdUpdate : function(cmp, event, helper) {
        // Get the Id from the Event
        var accountId = event.getParam("sObjectId");

        // Set the Id bound to the View
        cmp.set('v.recordId', accountId);
    },

    /**
     * Handler for receiving the clearLookupIdEvent event
     */
    handleAccountIdClear : function(cmp, event, helper) {
        // Clear the Id bound to the View
        cmp.set('v.recordId', null);
    }
})

If you want to plug the component into Lightning builder, you’ll need to include a design in the component bundle. Since there are no attributes that need passing into our Account Lookup component it just looks like this:

Design

<design:component> 
</design:component>

Creating the Lightning Component Tab

  • Navigate to: Setup->Create->Tabs
  • Click on New Lightning Component Tab.
  • Select the AccountLookup from the dropdown list.
  • Enter a Tab Name.
  • Choose a Tab Style.

… and you’re nearly good to go.

All that remains is to add it to the Salesforce1 Navigation.

Navigate to: Setup->Mobile Administration->Salesforce1 Navigation

Add the Component tab to your navigation, fire up the Salesforce1 mobile app and give it a whirl!

Notes:

All the code samples assume the default namespace. If you have your own namespace you’ll need to change the references from c:mything to mynamespace:mything.

If you are using a different version of the SLDS, you’ll need to change the paths in the code from SLDS092 to match your version.

Code and Resources:

The code is based on Version 0.92 of the SLDS: https://www.lightningdesignsystem.com/resources/downloads

Github repository containing all the above code (Note: this includes some updates as per the discussions in the comments thread): https://github.com/tscottdev/Lightning-Lookup

Links and stuff:

Salesforce Lightning Design System

Trailhead: Build a Lightning App with the Lightning Design System

 

Tagged with: , , , ,
Posted in apex, code, force.com, lightning, salesforce

Salesforce Lightning Lookup Component

Lightning Yellow Pages

Note: This article has been superceded by: Version 2

I’ve been playing around with Salesforce Lightning components and one of the obvious things that seems to be missing is a UI component to lookup a record in a Salesforce custom object and return its Id.

This was really simple in Visualforce. You could just use an <apex:inputField> tag and bind it to an objects lookup field in your controller. Salesforce would take care of the rest. Lightning, however, is a different beast and centres around building client side apps or components in Javascript and utilising various API’s for dealing with any server side requests.

There’s quite a bit of code here so I’ve added it all to a github repository. (Links at the bottom)

Design

There were a few things to consider:

  • It should work both within a desktop app and within the Salesforce Lightning mobile app.
  • It should be intuitive and have the look and feel of a lightning app.
  • It should be reusable, easily configured and plumbed into another component or app.

Cake

As I said, there’s quite a bit of code so before we begin, like all good cake recipes, let’s look at the finished result (6Mb Animated GIF file, please be patient while it loads):

Ligntning Lookup Component

Ligntning Lookup Component

Look & Feel (using the Salesforce Lightning Design System)

The look and feel comes from the Salesforce Lightning Design System or SLDS for short. This is a prerequisite, so you’ll need to install this before you press ahead and load any of the code into your org. I’m using SLDS092 but you should be able to a later version if you wish. You can install it into your org using the links here. This library provides a whole plethora of CSS, images to make your app and components look like the real deal. It also covers the UI design and how to structure your markup. For the lookup component I’ve used the markup described in the Lookups component of the SLDS. In some cases I’ve replaced the standard HTML tags with lightning framework markup such as <ui:inputText> instead of <input>. I’ve also had to create an <c:svg> component to use instead of the <svg> tag which is not supported in lightning. This is described in the Trailhead module: Build a Lightning App with the Lightning Design System.

Show me the code!

Lightning component bundle: svg

First things first, I need to show you the svg component. You need this so you can render the icons as nice scalable graphics without using the <svg> tag. Now this is pretty much lifted straight from the trailhead module so I’m not going to bore you with how it works. You can check out the trailhead yourself.

Here’s the component markup:

Component

<aura:component >
  <aura:attribute name="class" type="String" description="CSS classname for the SVG element" />
  <aura:attribute name="xlinkHref" type="String" description="SLDS icon path. Ex: /assets/icons/utility-sprite/svg/symbols.svg#download" />
  <aura:attribute name="ariaHidden" type="String" default="true" description="aria-hidden true or false. defaults to true" />
</aura:component>

It has a renderer defined which looks like this:

Renderer

({
  render: function(component, helper) {
    //grab attributes from the component markup
    var classname = component.get("v.class");
    var xlinkhref = component.get("v.xlinkHref");
    var ariaHidden = component.get("v.ariaHidden");

    //return an svg element w/ the attributes
    var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute('class', classname);
    svg.setAttribute('aria-hidden', ariaHidden);
    svg.innerHTML = '<use xlink:href="'+xlinkhref+'"></use>';
    return svg;
  }
})

Apex Controller: LookupSObjectController

We’re going to need an apex controller on the server side to query the database and return the matching records. To do this create the following aura enabled Apex class:

Apex class

/**
 * Apex Controller for looking up an SObject via SOSL
 */
public with sharing class LookupSObjectController 
{
    /**
     * Aura enabled method to search a specified SObject for a specific string
     */
    @AuraEnabled
    public static Result[] lookup(String searchString, String sObjectAPIName)
    {
        // Sanitze the input
        String sanitizedSearchString = String.escapeSingleQuotes(searchString);
        String sanitizedSObjectAPIName = String.escapeSingleQuotes(sObjectAPIName);

        List<Result> results = new List<Result>();

        // Build our SOSL query
        String searchQuery = 'FIND \'' + sanitizedSearchString + '*\' IN ALL FIELDS RETURNING ' + sanitizedSObjectAPIName + '(id,name) Limit 50'; 

        // Execute the Query
        List<List<SObject>> searchList = search.query(searchQuery);

        // Create a list of matches to return
        for (SObject so : searchList[0])
        {
            results.add(new Result((String)so.get('Name'), so.Id));
        }
        
        return results;
    }
    
    /**
     * Inner class to wrap up an SObject Label and its Id
     */
    public class Result
    {
        @AuraEnabled public String SObjectLabel {get; set;}
        @AuraEnabled public Id SObjectId {get; set;}
        
        public Result(String sObjectLabel, Id sObjectId)
        {
            this.SObjectLabel = sObjectLabel;
            this.SObjectId = sObjectId;
        }
    }
}

Notice the @AuraEnabled keyword. This allows the method to be called from the lightning framework. The lookup method constructs a SOSL query using the passed in search string and the objects API name. It returns a list of Results that contain a label and the Id of the matching SObjects.

Lightning component bundle: LookupSObject

On the client side we have a reusable Lightning component. The component contains attributes to specify the SObjects API Name, the label and plural label to apply along with an optional SVG image path and class from the SLDS. If the latter aren’t specified it uses a default icon, in this case a star.

There are two events registered: updateLookupIdEvent and clearLookupIdEvent. The former is fired when the user chooses an entry from the list, the latter is fired when the user clears a selection (via the cross icon.)

The component uses a keyup event to pass the user entered search string to a controller method. This in turn calls the server side Aura Enabled method.

The markup generated adheres to the SLDS Lookup component design. Note the <ul> tag. This is where the <li> tags will be inserted for the records matching the search.

The component markup looks like this:

Component

<aura:component controller="LookupSObjectController" >
    <!-- Required Scripts/Styles -->
    <!-- Salesforce Lightning Design System : https://www.lightningdesignsystem.com/ -->
    <ltng:require styles="/resource/SLDS092/assets/styles/salesforce-lightning-design-system-ltng.css" />
    
    <!-- Attributes -->
    <aura:attribute name="sObjectAPIName" type="String" required="true" 
        description="The API name of the SObject to search" />
    <aura:attribute name="label" type="String" required="true" 
        description="The label to assign to the lookup, eg: Account" />
    <aura:attribute name="pluralLabel" type="String" required="true" 
        description="The plural label to assign to the lookup, eg: Accounts" />
    <aura:attribute name="listIconSVGPath" type="String" default="/resource/SLDS092/assets/icons/custom-sprite/svg/symbols.svg#custom11" 
        description="The static resource path to the svg icon to use." />
    <aura:attribute name="listIconClass" type="String" default="slds-icon-custom-11" 
        description="The SLDS class to use for the icon." />
    <aura:attribute name="searchString" type="String" 
        description="The search string to find." />

    <!-- Events -->
    <aura:registerEvent name="updateLookupIdEvent" type="c:UpdateLookupId"/>    
    <aura:registerEvent name="clearLookupIdEvent" type="c:ClearLookupId"/>    
    
    <!-- Lookup Markup : See https://www.lightningdesignsystem.com/components/lookups -->
    <div class="slds"> 
        <div aura:id="lookup-div" class="slds-lookup" data-select="single" data-scope="single" data-typeahead="true">
            <!-- This is the Input form markup -->
            <div class="slds-form-element">
                <label class="slds-form-element__label" for="lookup">{!v.label}</label>
                <div class="slds-form-element__control slds-input-has-icon slds-input-has-icon--right">
                    <c:svg class="slds-input__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                    <!-- This markup is for when an item is currently selected -->
                    <div aura:id="lookup-pill" class="slds-pill-container slds-hide">
                        <span class="slds-pill slds-pill--bare">
                            <span class="slds-pill__label">
                                <c:svg class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!v.searchString}
                            </span>
                            <button class="slds-button slds-button--icon-bare" onclick="{!c.clear}">
                                <c:svg class="slds-button__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#close" />
                                <span class="slds-assistive-text">Remove</span>
                            </button>
                        </span>
                    </div>
                    <!-- This markup is for when searching for a string -->
                    <ui:inputText aura:id="lookup" value="{!v.searchString}" class="slds-input" updateOn="keyup" keyup="{!c.search}" />
                </div>
            </div>
            <!-- This is the lookup list markup. Initially it's hidden -->
            <div aura:id="lookuplist" class="slds-lookup__menu slds-hide" role="listbox">
                <div class="slds-lookup__item">
                    <button class="slds-button">
                        <c:svg class="slds-icon slds-icon-text-default slds-icon--small" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                        &quot;{!v.searchString}&quot; in {!v.pluralLabel}
                    </button>
                </div>
                <ul aura:id="lookuplist-items" class="slds-lookup__list" role="presentation">
                </ul>
            </div>
        </div>
    </div>

</aura:component>

There is an associated controller to handle the events. The search method, as mentioned earlier, is called from the keyup event on the search. The select method is called when the user chooses a result from the rendered list. The clear method is called when the user clears the selected item by clicking the cross icon:

Controller

({
    /**
     * Search an SObject for a match
     */
    search : function(cmp, event, helper) {
        helper.doSearch(cmp);        
    },

    /**
     * Select an SObject from a list
     */
    select: function(cmp, event, helper) {
        helper.handleSelection(cmp, event);
    },
    
    /**
     * Clear the currently selected SObject
     */
    clear: function(cmp, event, helper) {
        helper.clearSelection(cmp);    
    }
})

You’ll notice that each of the methods delegate to a helper. The helper does all the grunt work and looks like this:

Helper

({
    /**
     * Perform the SObject search via an Apex Controller
     */
    doSearch : function(cmp) {
        // Get the search string, input element and the selection container
        var searchString = cmp.get("v.searchString");
        var inputElement = cmp.find('lookup');
        var lookupList = cmp.find("lookuplist");
        var lookupListItems = cmp.find("lookuplist-items");

        // Clear any errors and destroy the old lookup items container
        inputElement.set('v.errors', null);
        lookupListItems.set('v.body', new Array());
        
        // We need at least 2 characters for an effective search
        if (typeof searchString === 'undefined' || searchString.length < 2)
        {
            // Hide the lookuplist
            $A.util.addClass(lookupList, 'slds-hide');
            return;
        }

        // Show the lookuplist
        $A.util.removeClass(lookupList, 'slds-hide');

        // Get the API Name
        var sObjectAPIName = cmp.get('v.sObjectAPIName');

        // Create an Apex action
        var action = cmp.get("c.lookup");

        // Mark the action as abortable, this is to prevent multiple events from the keyup executing
        action.setAbortable();

        // Set the parameters
        action.setParams({ "searchString" : searchString, "sObjectAPIName" : sObjectAPIName});
                          
        // Define the callback
        action.setCallback(this, function(response) {
            var state = response.getState();

            // Callback succeeded
            if (cmp.isValid() && state === "SUCCESS")
            {
                // Get the search matches
                var matches = response.getReturnValue();

                // If we have no matches, return
                if (matches.length == 0)
                {
                    return;
                }
                
                // Render the results
                this.renderLookupComponents(cmp, lookupListItems, matches);
            }
            else if (state === "ERROR") // Handle any error by reporting it
            {
                var errors = response.getError();
                
                if (errors) 
                {
                    if (errors[0] && errors[0].message) 
                    {
                        this.displayToast('Error', errors[0].message);
                    }
                }
                else
                {
                    this.displayToast('Error', 'Unknown error.');
                }
            }
        });
        
        // Enqueue the action                  
        $A.enqueueAction(action);                
    },

    /**
     * Render the Lookup List Components
     */    
    renderLookupComponents : function(cmp, lookupListItems, matches)
    {
        // list Icon SVG Path and Class
        var listIconSVGPath = cmp.get('v.listIconSVGPath');
        var listIconClass = cmp.get('v.listIconClass');

        // Array of components to create
        var newComponents = new Array();
         
        // Add a set of components for each match found
        for (var i=0; i<matches.length; i++)
        {
            // li element
            newComponents.push(["aura:html", {
                "tag" : "li",
                "HTMLAttributes" : {
                    "class" : "slds-lookup__item" 
                }
            }]);

            // a element
            newComponents.push(["aura:html", {
                "tag" : "a",
                "HTMLAttributes" : { 
                    "id" : cmp.getGlobalId() + '_id_' + matches[i].SObjectId, 
                    "role" : "option", 
                    "onclick" : cmp.getReference("c.select") 
                }
            }]);

            // svg component
            newComponents.push(["c:svg", {
                "class" : "slds-icon " + listIconClass + " slds-icon--small",
                "xlinkHref" : listIconSVGPath
            }]);

            // output text component
            // For some reason adding an aura:id to this component failed to record the id for subsequent cmp.find requests
            newComponents.push(["ui:outputText", {
                "value" : matches[i].SObjectLabel
            }]);
        }

        // Create the components
        $A.createComponents(newComponents, function(components, status) {
            // Creation succeeded
            if (status === "SUCCESS")
            {
                // Get the List Component Body
                var lookupListItemsBody = lookupListItems.get('v.body');

                // Iterate the created components in groups of 4, correctly parent them and add them to the list body
                for (var i=0; i<components.length; i+=4)
                {
                    // Identify the releated components
                    var li = components[i];
                    var a = components[i+1];
                    var svg = components[i+2];
                    var outputText = components[i+3];

                    // Add the <a> to the <li>
                    var liBody = li.get('v.body');
                    liBody.push(a);
                    li.set('v.body', liBody);

                    // Add the <svg> and <outputText> to the <a>
                    var aBody = a.get('v.body');
                    aBody.push(svg);
                    aBody.push(outputText);
                    a.set('v.body', aBody);

                    // Add the <li> to the container
                    lookupListItemsBody.push(li);
                }

                // Update the list body
                lookupListItems.set('v.body', lookupListItemsBody);
           }
           else // Report any error
           {
                this.displayToast('Error', 'Failed to create list components.');
           }
        });

    },

    /**
     * Handle the Selection of an Item
     */
    handleSelection : function(cmp, event) {
        // Resolve the Object Id from the events Element Id (this will be the <a> tag)
        var objectId = this.resolveId(event.currentTarget.id);

        // The Object label is the 2nd child (index 1)
        var objectLabel = event.currentTarget.children[1].innerText;

        // Log the Object Id and Label to the console
        console.log('objectId=' + objectId);
        console.log('objectLabel=' + objectLabel);
                
        // Create the UpdateLookupId event
        var updateEvent = cmp.getEvent("updateLookupIdEvent");
        
        // Populate the event with the selected Object Id
        updateEvent.setParams({
            "sObjectId" : objectId
        });

        // Fire the event
        updateEvent.fire();

        // Update the Searchstring with the Label
        cmp.set("v.searchString", objectLabel);

        // Hide the Lookup List
        var lookupList = cmp.find("lookuplist");
        $A.util.addClass(lookupList, 'slds-hide');

        // Hide the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.addClass(inputElement, 'slds-hide');

        // Show the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.removeClass(lookupPill, 'slds-hide');

        // Lookup Div has selection
        var inputElement = cmp.find('lookup-div');
        $A.util.addClass(inputElement, 'slds-has-selection');

    },

    /**
     * Clear the Selection
     */
    clearSelection : function(cmp) {
        // Create the ClearLookupId event
        var clearEvent = cmp.getEvent("clearLookupIdEvent");
        
        // Fire the event
        clearEvent.fire();

        // Clear the Searchstring
        cmp.set("v.searchString", '');

        // Hide the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.addClass(lookupPill, 'slds-hide');

        // Show the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.removeClass(inputElement, 'slds-hide');

        // Lookup Div has no selection
        var inputElement = cmp.find('lookup-div');
        $A.util.removeClass(inputElement, 'slds-has-selection');
    },

    /**
     * Resolve the Object Id from the Element Id by splitting the id at the _
     */
    resolveId : function(elmId)
    {
        var i = elmId.lastIndexOf('_');
        return elmId.substr(i+1);
    },

    /**
     * Display a message
     */
    displayToast : function (title, message) 
    {
        var toast = $A.get("e.force:showToast");

        // For lightning1 show the toast
        if (toast)
        {
            //fire the toast event in Salesforce1
            toast.setParams({
                "title": title,
                "message": message
            });

            toast.fire();
        }
        else // otherwise throw an alert
        {
            alert(title + ': ' + message);
        }
    }
})

The doSearch method takes care of calling the Apex controller and handling the results. It calls out to the renderLookupComponents method which creates and injects the <li> elements within the <ul> element of the component markup. We render a <a> element within the <li> element to fire the selection event. We make use of the SObjects Id within the rendered element id so that we can easily capture it when the item is selected. The resolveId method splits the SObject Id from the end of the element id.

The handleSelection method takes the selected entry and fires the updateLookupIdEvent before modifying the CSS classes to show the Lookup in its selected state.

The clearSelection method fires the clearLookupIdEvent before modifying the CSS classes to revert back to the search state.

The displayToast is simply an error logging method that renders a toast popup in the Salesforce1 app or an alert in the browser.

Lightning event: UpdateLookupId

This is the event fired when the user selects an item from search results. It has a single attribute that gets set with the Id of the selected record. You’ll notice this is a component event rather than an application event. Be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Update Lookup Id" >
    <aura:attribute name="sObjectId" type="Id" required="true" description="The Id of the selected SObject." />
</aura:event>

Lightning event: ClearLookupId

This is the event fired when the user clears the selected item. It has no attributes. Again, you’ll notice this is a component event rather than an application event. Once again, be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Clear the Lookup" />

Ok, great. Now can I see how you use it?

We’re going to wrap this up with an example Account Lookup component which you can plug straight into Salesforce1 via a Component Tab. Since the component implements flexipage:availableForAllPageTypes you could plug it straight into Lightning Builder if you wished. If you wanted to embed it in a Visualforce page you could do that as well. I won’t go into how to do that here but you’ll find details in the Lightning developer documentation.

Lightning component: AccountLookup

First lets look at the component markup. There’s an attribute to hold the Record Id. This is hooked up to the force:recordView component which is only rendered if an Id is present. We have two event handlers listening for the UpdateLookupId and ClearLookupId events. Note the names tie up to the registered events in the LookupSObject component. These event handlers hook up to the components controller via the handleAccountIdUpdate and handleAccountIdClear methods. The LookupSObject component markup takes the labels and icon resources as attributes.

Component

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes">
    <!-- Attributes -->
    <aura:attribute name="recordId" type="Id" description="The current record Id to display" />
    
    <!-- Event handlers -->
    <aura:handler name="updateLookupIdEvent" event="c:UpdateLookupId" action="{!c.handleAccountIdUpdate}"/>
    <aura:handler name="clearLookupIdEvent" event="c:ClearLookupId" action="{!c.handleAccountIdClear}"/>
    
    <!-- Lookup component -->
    <c:LookupSObject label="Account" pluralLabel="Accounts" sObjectAPIName="Account" 
        listIconSVGPath="/resource/SLDS092/assets/icons/standard-sprite/svg/symbols.svg#account"
        listIconClass="slds-icon-standard-account"
    />

    <!-- Record view -->
    <aura:if isTrue="{!v.recordId!=null}">
        <force:recordView recordId="{!v.recordId}" />
    </aura:if>
</aura:component>

Now lets take a look at the controller. You’ll see the handleAccountIdUpdate method gets the Id from the Event and updates the recordId attribute. The handleAccountIdClear simply nulls the recordId attribute. And that’s it, simples.

Controller

({
    /**
     * Handler for receiving the updateLookupIdEvent event
     */
    handleAccountIdUpdate : function(cmp, event, helper) {
        // Get the Id from the Event
        var accountId = event.getParam("sObjectId");

        // Set the Id bound to the View
        cmp.set('v.recordId', accountId);
    },

    /**
     * Handler for receiving the clearLookupIdEvent event
     */
    handleAccountIdClear : function(cmp, event, helper) {
        // Clear the Id bound to the View
        cmp.set('v.recordId', null);
    }
})

If you want to plug the component into Lightning builder, you’ll need to include a design in the component bundle. Since there are no attributes that need passing into our Account Lookup component it just looks like this:

Design

<design:component> 
</design:component>

Creating the Lightning Component Tab

  • Navigate to: Setup->Create->Tabs
  • Click on New Lightning Component Tab.
  • Select the AccountLookup from the dropdown list.
  • Enter a Tab Name.
  • Choose a Tab Style.

… and you’re nearly good to go.

All that remains is to add it to the Salesforce1 Navigation.

Navigate to: Setup->Mobile Administration->Salesforce1 Navigation

Add the Component tab to your navigation, fire up the Salesforce1 mobile app and give it a whirl!

Notes:

All the code samples assume the default namespace. If you have your own namespace you’ll need to change the references from c:mything to mynamespace:mything.

If you are using a different version of the SLDS, you’ll need to change the paths in the code from SLDS092 to match your version.

Code and Resources:

The code is based on Version 0.92 of the SLDS: https://www.lightningdesignsystem.com/resources/downloads

Github repository containing all the above code (Note this is now Version 2): https://github.com/tscottdev/Lightning-Lookup

Links and stuff:

Salesforce Lightning Design System

Trailhead: Build a Lightning App with the Lightning Design System

 

Tagged with: , , , ,
Posted in apex, code, force.com, lightning, salesforce

Salesforce Lightning support in Sublime Text 3 for Mac OSX

ToolsIf you’re developing Salesforce Lightning components you’ll probably reach a stage where you get fed up of using the Dev Console. Right now the Eclipse based IDE doesn’t support lightning and Joe Ferraro is still working on the latest updates to MavensMate.

One option is to use Dave Carrol‘s Lightning Tools plugin for Sublime Text. I had a bit of a headache getting this to work on my Mac and with a bit of googling have finally succeeded so I just wanted to wrap it all up here.

Firstly I’m using Sublime Text 3 with MavensMate v6 installed on a Mac running El Capitan. So I’m assuming you’re at the same starting point.

  1. Firstly you need to install the plugin:
    • Open Sublime Text.
    • Open the menu: Suplime Text -> Preferences -> Package Control
    • Select: Package Control: Install Package
    • Enter lightning when prompted.
    • Ignore the error: Sublime Lightning Plugin requires the Force.com CLI to function
    • Quit Sublime.
  2. Next you’ll need the Force.com Command Line Interface:
  3. Create a symbolic link to Sublime in /usr/local/bin:
    • Open Terminal.
    • Enter the command:
      sudo ln -s “/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl” /usr/local/bin/subl
  4. Create a symbolic link to the Force.com CLI in /usr/local/bin:
    • Open Terminal.
    • Enter the command:
      sudo ln -s /mypath/force /usr/local/bin/force
      (replace /mypath with the path you saved the Force.com CLI in)
  5. Run sublime from the Terminal:
    • Open Terminal
    • Enter the command:
      subl
    • You should no longer get the error: Sublime Lightning Plugin requires the Force.com CLI to function
  6. Use the Lightning right-click context menu on your Salesforce project to Login and work with your lightning components.
    • Lightning Context Menu

The extra hoops are because Sublime requires the tools to be available in the path. If you launch Sublime outside of Terminal the path is ignored and the error message is generated.

There is a sublime text plugin that allows the path to be specified called SublimeFixMacPath but I couldn’t get it to work in El Capitan.

While it’s not exactly elegant launching from the command line it’s a reasonable workaround for now.

Tagged with: , , , , ,
Posted in code, force.com, salesforce

Managing the heap using SOQL For Loops (with a little code seperation)

cairnI’m a big fan of code separation and code reuse. When I’m coding I like to keep my SOQL out of the business logic and in a gateway class of its own. That way I can reuse common queries in my code and keep a seperate database layer of sorts. If I need to modify my queries due to new custom fields I know straight where to go.

When working with large data sets it’s important to manage the heap which is one of the benefits you get from using a SOQL For Loop. However that presents a challenge in separating business logic and database access since the business logic must be contained within the loop.

Consider a basic query that loads opportunities for a given account. I would put this in a gateway class like this:

public with sharing class OpportunityGateway
{
	public static List<Opportunity> getOpportunitiesByAccountId(Id accountId) 
	{
		return [Select Id, Name, StageName
				From Opportunity 
				Where AccountId = :accountId];
	}
}

I might have a service class that sets all Opportunities to Closed Won for a given account:

public with sharing class OpportunityService
{
	public static Integer closeOpportunities(Id accountId) 
	{
		// Load the Opportunities for the Account
		List<Opportunity> opps = OpportunityGateway.getOpportunitiesByAccountId(accountId);

		// Iterate the Opportunities and set the Stage to Closed Won
		for(Opportunity opp : opps)
		{
			opp.StageName = 'Closed Won';
		}

		// Update the Opportunities
		update opps;

		System.debug('Heap Size: ' + Limits.getHeapSize());

		return opps.size();
	}
}

So the problem with this is that if I have a lot of Opportunities associated with my Account I’m going to consume a lot of heap. This is because Salesforce will load the entire result set into the List.

To demonstrate, I have the following test class that creates 400 opportunities and then uses the above service to update them:

@isTest
private class OpportunitySericeTest
{
	@isTest
	static void testCloseOpportunities()
	{
		Account acc = [Select Id From Account Where Name = 'Test Account'];

		Integer updateCount = OpportunityService.closeOpportunities1(acc.Id);
		
		System.assertEquals(400, updateCount);
	}
	
	@testSetup 
	static void createTestData()
	{
		Account acc = new Account(Name = 'Test Account');
		insert acc;

		List<Opportunity> opps = new List<Opportunity>();

		for(Integer i=0; i<400; i++)
		{
			opps.add(
				new Opportunity(
					AccountId = acc.Id,
					Name = 'Test Opportunity ' + i+1,
					StageName = 'Prospecting',
					CloseDate = System.today()
				)
			);
		}

		insert opps;
	}
}

Running the test and looking at the debug log I can see how much heap this has consumed:

DEBUG|Heap Size: 36273

The solution to this is to use a SOQL For Loop to chunk the large data set and therefore reduce the heap by working on a subset of records at a time.

But wait, I still want to retain my code seperation and keep my SOQL in my gateway class.

To do this I introduce a simple interface:

public interface IQueryHandler
{
	void execute(List<SObject> scope);
}

I then modify my gateway class to make use of a SOQL For Loop and delegate the contents to an implementation of my interface rather than return the result set:

public with sharing class OpportunityGateway
{
	public static void getOpportunitiesByAccountId(Id accountId, IQueryHandler handler)
	{
		for (List<Opportunity> opps : [Select Id, Name, StageName
										From Opportunity 
										Where AccountId = :accountId])
		{
			handler.execute(opps);
		}
	}
}

My service class looks a little different too as I need to pass an implementation of the IQueryHandler interface to the gateway method. To do this I use a private inner class within the service class:

public with sharing class OpportunityService
{
	public static Integer closeOpportunities(Id accountId) 
	{
		// Create the Query Handler
		CloseOpportunitiesQueryHandler queryHandler = new CloseOpportunitiesQueryHandler();

		// Process the Opportunities for the Account using the Query Handler
		OpportunityGateway.getOpportunitiesByAccountId(accountId, queryHandler);

		System.debug('Heap Size: ' + Limits.getHeapSize());
		System.debug('Max Heap Size: ' + queryHandler.getMaxHeap());

		return queryHandler.getRowCount();
	}

	private class CloseOpportunitiesQueryHandler
		implements IQueryHandler
	{
		private Integer m_rowCount;
		private Integer m_maxHeap;

		// Class Constructor
		public CloseOpportunitiesQueryHandler()
		{
			m_rowCount = 0;
			m_maxHeap = 0;
		}

		// Interface method takes a List of SOBjects
		public void execute(List<SObject> scope)
		{
			// Iterate the Opportunities and set the Stage to Closed Won
			for(Opportunity opp : (List<Opportunity>)scope)
			{
				opp.StageName = 'Closed Won';
			}

			// Update the Opportunities
			update scope;

			m_rowCount += scope.size();
			
			Integer heapSize = Limits.getHeapSize();
			m_maxHeap = heapSize > m_maxHeap ? heapSize : m_maxHeap;
		}

		public Integer getRowCount()
		{
			return m_rowCount;
		}

		public Integer getMaxHeap()
		{
			return m_maxHeap;
		}
	}
}

Just to make sure this acheives the benefit I’m looking for I can run the unit test again and check the results:

DEBUG|Heap Size: 1322
DEBUG|Max Heap Size: 18881

As you can see the the heap consumed at the end of the operation is significantly less using the SOQL For Loop. Even during the processing of the query it peaks around half of the original implentation. Considering the SOQL For Loop chunks the records into batches of 200 and we are processing 400 records this is what we would expect.

A couple of things to take note of:

  • Be aware of governor limits, in particular the number of query rows and DML operations. This pattern does a single update within a SOQL For Loop. The query will chunk at 200 records per iteration. This would, in theory, allow 30000 records to be updated using the maximum of 150 DML statements. However, the maximum number of DML rows that can be processed is 10000 so in reality if you have more than 10000 records, you’ll be in trouble way before DML statments.
  • If you are going to be processing a large number of records you may wish to look at using batch apex instead.

Further reading:

Salesforce.com : Working with Very Large SOQL Queries

Salesforce.com : Execution Governors and Limits

Tagged with: , , , ,
Posted in apex, code, force.com, salesforce

Pebble Watch: Salesforce Approvals – Part 2

Watchface

In Part 1, we looked at creating the Salesforce server application to expose the Approvals and the target objects via some REST based web services.

In the second part of this article we look at creating the Pebble watch app and hooking it up the web services.

Creating a connected Application

The first thing we need to do is create a connected application in Salesforce.

To do this, login to Salesforce and navigate to:

Setup->Create->Apps->Connected Apps and create a new App.

Give it an App Name, API Name and Contact Email.

Check the Enable OAuth Settings and enter a Callback URL of sfdc://success and finally make sure Access and manage your data (api) is in the Selected OAuth Scopes. Since you will be connecting via your mobile phone you should also set IP Relaxation to Relax IP restrictions.

Save your changes.

You’re going to need 2 pieces of information from the Connected App, the Consumer Key and Consumer Secret. For now just remember where to find them as you’ll need them later.

Creating the Pebble Watch App

If you havn’t already got one, you’ll need to sign up for a Pebble Cloud Developer Account at: https://cloudpebble.net/

Create a new Project of type: Pebble.js

You’re going to need a bunch of images. You’ll find all these in the GitHub at https://github.com/tscottdev/PebbleSFDCApprovals/tree/master/resources/images

Click the Add New button next to the Project Resources and upload them.

Next we’ll need to configure the Project a little. Click on the Settings menu.

Make sure the App Kind is set to Watchapp, the Menu Image is checkbox28x28.png and the Configurable checkbox is checked.

Adding the Configuration

In order to supply user credentials to Salesforce we are going to make the app configurable. This allows the user to configure the application via the Pebble companion app:

Pebble Companion App Pebble Configuration

The user clicks the gear icon in the companion app and is presented with a configuration page. The configuration page is an html page hosted on your own web server. For this exercise I’ve hosted the html page in the public folder of my dropbox account and used the public URL to access it.

The html configuration page looks like this:

<!DOCTYPE html>
<html>
<head>
	<title>SFDC Approvals - Configuration</title>
</head>

<style>
	body {
		font-size: 12pt;
		color: white;
		background-color: black;
	}
	h1 {
		font-size: 14pt;
	}
	input {
		width: 100%;
	}
</style>

<script>
	// Get the Query Parameter
	function getQueryParam(variable, default_)
	{
		var query = location.search.substring(1);

		var vars = query.split('&');

		for (var i = 0; i < vars.length; i++)
		{
			var pair = vars[i].split('=');

			if (pair[0] == variable)
			{
				return decodeURIComponent(pair[1]);
			}
		}

		return default_ || false;
	}

	function save()
	{
		var unElm = document.getElementById('un');
		var pwElm = document.getElementById('pw');

		var configuration = { un: unElm.value, pw: pwElm.value };

		// Close config page and return data
		var return_to = getQueryParam('return_to', 'pebblejs://close#');
		location.href = return_to + encodeURIComponent(JSON.stringify(configuration));
	}
</script>

<body>
	<h1>SFDC Approvals - Configuration</h1>

	<form>
		User name:<br>
		<input type="text" name="un" id="un">
		<br>
		User password:<br>
		<input type="password" name="pw" id="pw">
		<br>
		<button type="button" onclick="save();">Save</button>
	</form>

</body>

</html>

The configuration page prompts for a username and password and passes it back to a local address as a JSON string. There’s some code in there to handle the call back address for the emulator as well as the companion app. We’ll see how we hook this up when we get onto the watch code.

Pebble Watch Code

First up we’ll create a new Javascript file called app-settings.js from within the CloudPebble IDE. We’ll use this to store a few constants, including the Salesforce App Client Id (Consumer Key) and Client Secret (Consumer Secret) from earlier. The main app will add an include to this file.

You’ll need to modify some of these values to suit your org:

/**
 * App Settings - You will need to replace this with values applicable to your org
 */
var appSettings = {
    // URL to the page hosting the Apps Config Screen
    configURL: 'https://dl.dropboxusercontent.com/u/XXXXXX/PebbleApps/SFDCApprovals/config-page.html',

    // The Client Id and Secret of your Connected App
    clientId: 'XXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXX.XXXXXXXX',
    clientSecret: 'XXXXXXXXXXXXXXXXXXX',

    // The URL prefix for the REST bases service calls, amend your instance and namespace accordingly
    serviceURLprefix: 'https://na10.salesforce.com/services/apexrest/mynamespace/'
};

this.exports = appSettings;

We’ll now create another Javascript file in the CloudPebble IDE called app.js. This is the main application code:

/**
 * (c) Tony Scott. Licensed under a Creative Commons Attribution 3.0 Unported License.
 * http://creativecommons.org/licenses/by/3.0/deed.en_US
 *
 * This software is provided as is, at your own risk and without warranty of any kind.
 *
 * Please visit my blog: https://meltedwires.com/ and 'like' if you've found this useful.
 */

// Required Libraries
var UI = require('ui');
var ajax = require('ajax');
var Vector2 = require('vector2');
var Settings = require('settings');
var AppSettings = require('app-settings');

// Salesforce Token (returned by the login function)
var token = '';

// Main Card
var main = new UI.Card({
    title: 'SFDC',
    subtitle: 'Approvals',
    icon: 'images/checkbox28x28.png',
    body: 'Connecting ...',
    action: { select: 'images/reload16x16.png' }
});

// Add Event listners for Configuration Screen to get and store the User and Password
Pebble.addEventListener('showConfiguration', function(e) {
    // Show config page
    console.log('AppSettings.configURL=' + AppSettings.configURL);
    Pebble.openURL(AppSettings.configURL);
});

Pebble.addEventListener('webviewclosed', function(e) {
    console.log('Configuration window returned: ' + e.response);

    var configuration = JSON.parse(decodeURIComponent(e.response));

    Settings.option('un', configuration.un);
    Settings.option('pw', configuration.pw);
});

/**
 * Salesforce login function
 *
 * Arguments: success (function)   Function to call upon successful login
 *            fail    (function)   Function to call upon failed login
 */
function login(success, fail)
{
    // OAuth login URL
    var url = 'https://login.salesforce.com/services/oauth2/token';

    // Construct the body of the request
    var body = 'grant_type=password' +
                '&client_id=' + AppSettings.clientId +
                '&client_secret=' + AppSettings.clientSecret +
                '&username=' + Settings.option('un') +
                '&password=' + Settings.option('pw');

    // Attempt the login and call the success or failiure function
    ajax({ url: url, method: 'post', type: 'text', data: body,
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
        },
        function(data) {
            // Success, store the token
            var obj = JSON.parse(data);

            token = obj.access_token;

            console.log('Login successful: ' + token);

            success();
        },
        function(error) {
            // Failure!
            var errorText = JSON.stringify(error);

            console.log('login failed: ' + errorText);

            fail(errorText);
        }
    );
}

/**
 * Load the Approvals via the REST Sevice
 */
function loadApprovals()
{
    // Indicate the status on the main screen
    main.body('Loading ...');

    // Create the request
    var req = {
        Username: Settings.option('un'),
    };

    console.log('req: ' + JSON.stringify(req));

    // Call the REST service and handle the success or failure
    ajax({ url: AppSettings.serviceURLprefix + 'GetWorkItems/',
          method: 'post', type: 'json', data: req,
          headers: { 'content-type': 'application/json', 'Authorization' : 'Bearer ' + token },
        },
        function(res) {
            // Success, render the approvals
            console.log('res: ' + JSON.stringify(res));

            main.body('Loaded.');
            renderApprovals(res);
        },
        function(error) {
            // Failure!
            var errorText = JSON.stringify(error);
            console.log('request failed: ' + errorText);

            main.body('Failed:\n' + errorText);
            main.action('select', 'images/reload16x16.png');
        }
    );
}

/**
 * Render the Approvals
 *
 * Arguments:    res    (object)    JSON Response from the GetWorkItems REST Service
 */
function renderApprovals(res)
{
    // If no work items, change the main window text and return
    if (res.WorkItems.length === 0)
    {
        main.body('Nothing to approve.');
        return;
    }

    // Create an array of Menu Items
    var menuItems = [];

    // Create the Menu Items from the Work Items returned by the web service
    for (var i = 0; i < res.WorkItems.length; i++)
    {
        var menuItem = {
            title: res.WorkItems[i].ObjectName,
            subtitle: res.WorkItems[i].WorkItemName,
            id: res.WorkItems[i].WorkItemId,
            fields: res.WorkItems[i].Fields
        };

        menuItems.push(menuItem);
    }

    console.log('menuItems=' + JSON.stringify(menuItems));

    // Add the Menu Items to the menu
    var menu = new UI.Menu({
        sections: [{
            title: 'Approval List',
            items: menuItems
        }]
    });

    // Add the on select event to show the Work Item in more detail
    menu.on('select', function(itemSelectEvent){
        showWorkItem(itemSelectEvent);
    });

    // Show the menu window
    menu.show();
}

/**
 * Show the Work Item in more detail
 *
 * Arguments:    itemSelectEvent    (object)     The menu select event
 */
function showWorkItem(itemSelectEvent)
{
    // Get the menu item from the event
    var menuItem = itemSelectEvent.item;

    var bodyText = '';

    // Build the card body from the fields returned.
    for (var i = 0; i < menuItem.fields.length; i++)
    {
        bodyText += menuItem.fields[i].Name + ': ' + menuItem.fields[i].Value + '\n';
    }

    // There are several windows that will be created as the item
    // is approved, keep track of them to enable them to be removed
    var windowStack = [];

    // Create the Card
    var detailCard = new UI.Card({
        title: menuItem.title,
        subtitle: menuItem.subtitle,
        body: bodyText,
        scrollable: true,
        action: {
            select: 'images/rightarrow12x16.png',
            up: 'images/uparrow16x11.png',
            down: 'images/downarrow16x11.png'
        }
    });

    // Add it to the window stack
    windowStack.push(detailCard);

    // Add the event handler to proceed to the Approval or Rejection
    detailCard.on('click', 'select', function(){
        approveOrReject(itemSelectEvent, windowStack);
    });

    // Show the card
    detailCard.show();
}

/**
 * Approve or Reject the Work Item
 *
 * Arguments:    itemSelectEvent    (object)     The menu select event
 *               windowStack        (array)      The Array of windows relating to this Work Item
 */
function approveOrReject(itemSelectEvent, windowStack)
{
    // Create the Approve or Regject Card
    var approveOrRejectCard = new UI.Card({
        title: 'Approve?',
        action: {
            up: 'images/approve16x16.png',
            down: 'images/reject16x16.png'
        }
    });

    // Add it the stack
    windowStack.push(approveOrRejectCard);

    // Add the event handler for the Approval and Rejection
    approveOrRejectCard.on('click', 'up', function(){
        approveItem(itemSelectEvent, true, windowStack);
    });

    approveOrRejectCard.on('click', 'down', function(){
        approveItem(itemSelectEvent, false, windowStack);
    });

    // show the card
    approveOrRejectCard.show();
}

/**
 * Perform the Approve or Reject
 *
 * Arguments:    itemSelectEvent    (object)     The menu select event
 *               approve            (boolean)    True to approve, false to reject
 *               windowStack        (array)      The Array of windows relating to this Work Item
 */
function approveItem(itemSelectEvent, approve, windowStack)
{
    // Get the menu item from the menu event
    var menuItem = itemSelectEvent.item;

    // Render the Approved/Rejected icon window
    var iconWindow = renderApproved(approve, windowStack);

    // Create the request, from the work item id and the approve/reject boolean
    var req = {
        WorkItemId: menuItem.id,
        Approve: approve
    };

    console.log('req: ' + JSON.stringify(req));

    // Call the REST service and handle the success or failure
    ajax({ url: AppSettings.serviceURLprefix + 'ApproveWorkItem/',
          method: 'post', type: 'json', data: req,
          headers: { 'content-type': 'application/json', 'Authorization' : 'Bearer ' + token },
        },
        function(res) {
            // Success
            console.log('res: ' + JSON.stringify(res));

            // Call the function to clear the icon and hide all the windows in the stack
            iconWindow.hideAll();

            // Remove the item form the menu
            itemSelectEvent.section.items.splice(itemSelectEvent.itemIndex, 1);

            // If no items left then hide the menu and change the main window text
            if (itemSelectEvent.section.items.length === 0)
            {
                main.body('Nothing to approve.');
                itemSelectEvent.menu.hide();
                return;
            }

        },
        function(error) {
            // Failure!
            var errorText = JSON.stringify(error);
            console.log('request failed: ' + errorText);

            // Hide the icon window and render the error
            iconWindow.hide();
            renderError(errorText);
        }
    );
}

/**
 * Render the approved / rejected icon
 *
 * Arguments:    approve        (boolean)    True to show the approve icon, false to show the reject icon
 *               windowStack    (array)      The Array of windows relating to the Work Item
 *
 * Returns:      (object)    The created window
 */
function renderApproved(approve, windowStack)
{
    // Create a new Window
    var win = new UI.Window();

    // Add it to the stack
    windowStack.push(win);

    // Assign the image
    var img = approve ? 'images/approve128x128.png' : 'images/reject128x128.png';

    // Create the image
    var image = new UI.Image({
        position: new Vector2(8, 10),
        size: new Vector2(128, 128),
        backgroundColor: 'white',
        image: img,
        compositing: 'invert'
    });

    // Add it to the window
    win.add(image);

    // Show the window
    win.show();

    // Create the hideAll fucntion to remove it
    win.hideAll = function(){
        // Animimate the image down
        var pos = image.position();
        pos.y = 169;

        image.animate('position', pos, 300);

        // Queue up the removal of the window stack when the
        // animation completes
        image.queue(function(next) {
            hideWindowStack(windowStack);
            next();
        });
    };

    // Return the window
    return win;
}

/**
 * Hide the window stack
 *
 * Arguments:    windowStack    (array)      The Array of windows relating to the Work Item
 */
function hideWindowStack(windowStack)
{
    while(windowStack.length > 0)
    {
        var win = windowStack.pop();
        win.hide();
    }
}

/**
 * Render an error
 *
 * Arguments:    err    (string)    The error to render
 */
function renderError(err)
{
    // Create the Card
    var errorCard = new UI.Card({
        title: 'Failed',
        body: err,
        scrollable: true
    });

    // Show the card
    errorCard.show();
}

// Show the Main Card
main.show();

// Login passing in success and failure functions
login(function(res) {
        // Show connected status
        main.body('Connected.');

        // Change the behaviour of the select button to load the approvals
        main.on('click', 'select', function() {
            loadApprovals();
        });

        loadApprovals();
    },
    function(errorText) {
        main.body('Failed:\n' + errorText);
    }
);

You’ll see near the top we have a couple of calls to add event listeners for the Configuration screen. These are used to marshall the username and password to the app storage.

The login to Salesforce is done using a grant_type of password and therefore bypasses the OAuth flow. This isn’t ideal but we can’t supply a call-back URL to the OAuth flow and even if we could we’d have to make the user login via the config screen whenever the session expires.

Once the login is complete we store the token to use for subsequent API requests for retrieving the Work Items to Approve and Approving/Rejecting the Work Items.

The Pebble windowing system is very simple and takes a stack approach. When a window is loaded it pushes the window onto a stack. Clicking the back button removes the current window from the stack and the previous window is shown as does hiding it via the application code.

The flow of the app looks like this:

Flow

The Watch App source is available from my GitHub repo: https://github.com/tscottdev/PebbleSFDCApprovals

I’ll make the Server code available soon or you can cut and paste the code from Part 1.

I’m considering moving the Client Id and Client Secret to the Configuration Screen at some point so that I can publish the App and make it freely available on the Pebble App store. Watch this space, I hope you’ve found this useful.

Useful Links:

CloudPebble IDE: https://cloudpebble.net/ide/

Pebble Developer Portal: https://developer.getpebble.com/

Pebble Store: https://getpebble.com/

 

Tagged with: , , , , ,
Posted in apex, code, force.com, pebble watch, salesforce

Pebble Watch: Salesforce Approvals – Part 1

Pebble Watch - ApprovalsI recently bought a Pebble ‘smart’ watch and I have to say I’m pretty impressed with it. Rather than try and replace the phone in your pocket, it works to extend it by providing you with notifications and an app store full of simple intuitive apps and watch faces. Unlike certain devices I could mention, you get days worth of battery life from a single charge!

The UI is small and simple (144 x 168 pixels) and navigation is performed with just four buttons: Back (top left), Up, Down and Select. It uses a Low Energy bluetooth connection to your phone running a companion app.

For the geekier folk there is a developer portal and cloud based IDE for creating your own apps and watch faces and I couldn’t resist!

Developing apps comes in two flavours: C and Javascript. C apps take a little more work and run faster while Javascript apps are quick and easy to develop.

Naturally I wanted to try and hook up my watch to Salesforce.

In this series of blog posts I’m going to show you a Pebble watch application that communicates with Salesforce.

Use Case

For my application I decided to base it around a real world use case that I felt fitted the Pebble watch:

“As a manager I want to be able to quickly approve or reject requests on the go or in a meeting without having to reach for my phone or my laptop.”

The above use case is ideal for a Pebble app. It wants to be quick, intuitive with a minimum of clicks.

The following is a check list of requirements:

  • I want to be notified when I have something to approve.
  • I want to be able see what items I have to approve.
  • I want to be able to see enough detail to make a decision.
  • I want to be able to quickly approve or reject an item.

The first point is taken care of by the Pebble notification system. Since a Salesforce approval request generates and sends an email this will be automatically sent to the watch provided your phone is configured to show email notifications.

User Interface

Jumping ahead I want to show you the finished UI and flow before I describe how you get there:

Flow

To summarise the above flow:

  • The app connects to Salesforce.
  • It requests and loads the items to approve.
  • It lists the items to approve.
  • The user then selects an item to approve (via the middle select button)
  • The user can then scroll up and down to view the detail of the item to approve.
  • Clicking the middle select button takes them to the decision.
  • Clicking ‘up’ approves and ‘down’ rejects and flow resumes from the list of items.
  • Clicking the top left ‘back’ button returns to the previous screen.
  • When no more items are left to approve the user is notified.
  • Clicking the middle select button from the main screen will reload the items to approve.

Solution Overview

There are two parts to this solution, a small Salesforce server application and a PebbleJS client side application.

The premise behind the Pebble watch is that applications are small and light. For that reason we need to keep the client side application pretty thin. We could invoke the standard Salesforce APIs from the watch app but it makes far more sense to do any grunt work on the server.

So for the server application we have an object to configure what fields the user can see in the detail window of the watch and a couple of REST based web services to retrieve the approvals and approve/reject item.

The client application then consumes these web services and deals with the navigation and rendering of the data.

I decided to write the client app using the PebbleJS library for ease and speed of development. The watch app makes use of the Pebble Ajax library to connect to Salesforce via the phones internet connection.

Server Side Application

Within Salesforce I’ve created an object called ‘Pebble Approval Field’. This object is used to administer the fields which will be exposed to the watch app. The tab looks like this:

Pebble Approval Fields  Pebble Approval Field item

It simply provides a list of object API names, field API names and the Sort Order. There are a couple of hidden fields which are populated by a Trigger. These are the Objects 3 character Key Prefix, Object Label and Field Label. We’ll see how these are used later. The full structure of the object looks like this:

Singular Label: Pebble Approval Field
Plural Label: Pebble Approval Fields
API Name: Pebble_Approval_Field__c

Field Label API Name Type
Field Id Name Autonumber
Object API Name Object_API_Name__c Text(40)
Field API Name Field_API_Name__c Text(40)
Sort Order Sort_Order__c Number(3,0)
Object Key Prefix Object_Key_Prefix__c Text(3) External Id
Object Label Object_Label__c Text(40)
Field Label Field_Label__c Text(40)

The principal Approval objects we are going to use are the ProcessInstanceWorkItem and its parent object ProcessInstance.

The ProcessInstanceWorkItem gives us the outstanding Approvals for a particular user while its parent gives us the Id of the object being approved. As this is just an object Id we can grab its 3 character prefix and use this to query the Pebble Approval Fields via the Object Key Prefix field.

To populate this we have a trigger to do a describe of the object and retrieve this. Since we are doing a describe anyway it made sense to grab and populate the Object and Field labels and save the service from having to worry about doing this. Object and Field label changes are infrequent so it doesn’t feel too bad to populate this in the before trigger.

The trigger code looks like this:

/**
 * Pebble_Approval_Field__c trigger to validate API names and populate key prefix
 */
trigger PebbleApprovalFieldTrigger on Pebble_Approval_Field__c (before insert, before update)
{
	// Set of System field Names
	Set<String> systemFields = new Set<String> {'CreatedById', 'CreatedDate', 'Id', 'IsDeleted', 'LastModifiedById', 'LastModifiedDate', 'Name', 'OwnerId', 'SystemModstamp'};

	// Start with a Global Describe
	Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();

	Boolean hasErrors = false;
	List<String> validObjectNames = new List<String>();

	// Validate the Object Names against the Describe
	for (Pebble_Approval_Field__c pebbleField : (List<Pebble_Approval_Field__c>)Trigger.new )
	{
		if (globalDescribe.containsKey(pebbleField.Object_API_Name__c))
		{
			validObjectNames.add(pebbleField.Object_API_Name__c);
		}
		else
		{
			pebbleField.Object_API_Name__c.addError('Object API name not found.');
			hasErrors = true;
		}
	}

	// If there are any object errors, quit now
	if (hasErrors)
	{
		return;
	}

	// Describe for all the Valid Objects
	Schema.DescribeSobjectResult[] objectDescribes = Schema.describeSObjects(validObjectNames);

	// Map the Describes Results
	Map<String, Schema.DescribeSobjectResult> objectDescribeMap = new Map<String, Schema.DescribeSobjectResult>();

	for (Schema.DescribeSobjectResult objectDescribe : objectDescribes)
	{
		objectDescribeMap.put(objectDescribe.getName(), objectDescribe);
	}

	// Validate the Field Names and populate the Key Prefix and Labels
	for (Pebble_Approval_Field__c pebbleField : (List<Pebble_Approval_Field__c>)Trigger.new )
	{
		// First check the field has the same namespace prefix as the object (if applicable)
		String[] parts = pebbleField.Object_API_Name__c.split('__');

		// Determine the Namespace
		String ns;

		if (parts.size() == 3)
		{
			ns = parts[0] + '__';

			// If its not a system field, it should contain the namespace
			if (!systemFields.contains(pebbleField.Field_API_Name__c) && !pebbleField.Field_API_Name__c.startsWith(ns))
			{
				pebbleField.Field_API_Name__c.addError('Field namespace is missing or does not match the object namespace.');
			}
		}

		// Now get the object describe
		Schema.DescribeSobjectResult objectDescribe = objectDescribeMap.get(pebbleField.Object_API_Name__c);

		Map<String, Schema.SObjectField> fieldMap = objectDescribe.fields.getMap();

		// Strip the Namespace from the Field Name before checking the field map
		String fieldName = ns == null ? pebbleField.Field_API_Name__c : pebbleField.Field_API_Name__c.removeStart(ns);

		if (!fieldMap.containsKey(fieldName))
		{
			pebbleField.Field_API_Name__c.addError('Field API name not found.');
		}

		// Populate the key prefix and labels
		Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldName).getDescribe();

		pebbleField.Object_Key_Prefix__c = objectDescribe.getKeyPrefix();
		pebbleField.Object_Label__c = objectDescribe.getLabel();
		pebbleField.Field_Label__c = fieldDescribe.getLabel();
	}
}

Get Work Items REST Web Service

The first web service is used to retrieve a list of Work Items to approve for a given user. It does this by querying the ProcessInstance/ProcessInstanceWorkItem object and the target objects data into an easy to consume response.

The request simply takes a username and the response returns a list of work items. Each work item is accompanied by a list of field labels and values.

Here’s the code:

/**
 * REST based web service to Get Work Items
 */
@RestResource(urlMapping='/GetWorkItems/*')
global without sharing class GetWorkItemsRESTService
{
	/**
	 * Service Entry point
	 */
	@HttpPost
	global static void doPost()
	{
		RestRequest restReq = RestContext.request;
		RestResponse restRes = RestContext.response;

		// JSON parse the body into a Request object
		Request req = (Request)JSON.deserialize(restReq.requestBody.toString(), Request.class);

		// Process the Request
		Response res = processRequest(req);

		// Serialize the Response
		restRes.responseBody = Blob.valueOf(JSON.serialize(res));
	}

	/**
	 * Process the Request
	 */
	private static Response processRequest(Request req)
	{
		// Get the user record
		User usr = findUserByUsername(req.Username);

		// get the work items and the target objects
		List<ProcessInstanceWorkitem> workItems = findWorkItemsByUserId(usr.Id);

		// Build a Map of Key Prefixes to Sets of Object Ids
		Map<String, Set<Id>> prefixIdMap = new Map<String, Set<Id>>();

		for (ProcessInstanceWorkitem item : workItems)
		{
			String keyPrefix = ('' + item.ProcessInstance.TargetObjectId).left(3);

			Set<Id> ids = prefixIdMap.get(keyPrefix);

			if (ids == null)
			{
				ids = new Set<Id>();
				prefixIdMap.put(keyPrefix, ids);
			}

			ids.add(item.ProcessInstance.TargetObjectId);
		}

		// Map the Pebble Approval fields by the SObject key prefixes
		Map<String, List<Pebble_Approval_Field__c>> fieldMap = mapPebbleApprovalFieldsByKeyPrefix(prefixIdMap.keySet());

		// Query and Map the target objects using the maps
		Map<Id, SObject> idMap = mapObjects(prefixIdMap, fieldMap);

		// build the response and return
		Response res = new Response();

		// Loop throught the Work Items
		for (ProcessInstanceWorkitem item : workItems)
		{
			// Get the Target Object from the Map
			SObject so = idMap.get(item.ProcessInstance.TargetObjectId);

			// if not found, skip as we can only pass back objects that have been mapped
			if (so == null)
			{
				continue;
			}

			// Get the key prefix
			String keyPrefix = ('' + item.ProcessInstance.TargetObjectId).left(3);

			// Get the fields from the Map
			List<Pebble_Approval_Field__c> fields = fieldMap.get(keyPrefix);

			// Create new Work Item to add to the response
			WorkItem wi = new WorkItem(item.Id, so.Id, fields[0].Object_Label__c, (String)so.get('Name'));

			for (Pebble_Approval_Field__c f : fields)
			{
				Field field = new Field(f.Field_Label__c, '' + formatObject(so.get(f.Field_API_Name__c)));
				wi.Fields.add(field);
			}

			res.WorkItems.add(wi);
		}

		return res;
	}

	/**
	 * Find the User record by the Username
	 */
	private static User findUserByUsername(String username)
	{
		try
		{
			return [Select Id From User Where Username = :username];
		}
		catch(System.QueryException e)
		{
			String error = 'User {0} not found.';
			throw new GetWorkItemsException(String.format(error, new String[] { username }));
		}
	}

	/**
	 * Get a list of Work Items by the User Id
	 */
	private static ProcessInstanceWorkitem[] findWorkItemsByUserId(Id userId)
	{
		return [Select Id, ActorId, CreatedDate, ProcessInstanceId, ProcessInstance.TargetObjectId
				From ProcessInstanceWorkitem
				Where ActorId = :userId
				Order By CreatedDate desc];
	}

	/**
	 * Load and map the Pebble Approval fields according to their Objects key prefix
	 */
	private static Map<String, List<Pebble_Approval_Field__c>> mapPebbleApprovalFieldsByKeyPrefix(Set<String> keyPrefixes)
	{
		Map<String, List<Pebble_Approval_Field__c>> rtnMap = new Map<String, List<Pebble_Approval_Field__c>>();

		for (Pebble_Approval_Field__c f : [Select Id, Name, Object_API_Name__c, Field_API_Name__c, Object_Key_Prefix__c, Sort_Order__c,
													Object_Label__c, Field_Label__c
												From Pebble_Approval_Field__c
												Where Object_Key_Prefix__c in :keyPrefixes
												Order By Object_API_Name__c, Sort_Order__c])
		{
			List<Pebble_Approval_Field__c> fields = rtnMap.get(f.Object_Key_Prefix__c);

			if (fields == null)
			{
				fields = new List<Pebble_Approval_Field__c>();
				rtnMap.put(f.Object_Key_Prefix__c, fields);
			}

			fields.add(f);
		}

		return rtnMap;
	}

	/**
	 * Load and map the target object records by their Id
	 */
	private static Map<Id, SObject> mapObjects(Map<String, Set<Id>> prefixIdMap, Map<String, List<Pebble_Approval_Field__c>> fieldMap)
	{
		Map<Id, SObject> rtnMap = new Map<Id, SObject>();

		// Loop through the key prefixes
		for (String keyPrefix : prefixIdMap.keySet())
		{
			// Get the fields for the prefix
			List<Pebble_Approval_Field__c> fields = fieldMap.get(keyPrefix);

			// if we have no fields, skip
			if (fields == null || fields.isEmpty())
			{
				continue;
			}

			// Query the objects and add them to the map
			rtnMap.putAll(query(fields, prefixIdMap.get(keyPrefix)));
		}

		return rtnMap;
	}

	/**
	 * Query the object using the field map for the supplied ids
	 */
	private static Map<Id, SObject> query(List<Pebble_Approval_Field__c> fields, Set<Id> objectIds)
	{
		// Build a Set of Fields (always include the Id and the Name)
		Set<String> fieldSet = new Set<String>{ 'Id', 'Name' };

		for (Pebble_Approval_Field__c f : fields)
		{
			fieldSet.add(f.Field_API_Name__c);
		}

		// Build the Query according to the fields
		String query = 'Select ';

		for (String f : fieldSet)
		{
			query += f + ',';
		}

		// Strip the trailing ,
		query = query.removeEnd(',');

		query += ' From ' + fields[0].Object_API_Name__c + ' Where Id in :objectIds';

		return new Map<Id, SObject>(Database.query(query));
	}

	/**
	 * Perform some rudimentary formatting depending on the objects type
	 */
	private static Object formatObject(Object o)
	{
		if (o == null)
		{
			return '';
		}

		if (o instanceOf Date)
		{
			return ((Date)o).format();
		}
		else if (o instanceOf DateTime)
		{
			return ((DateTime)o).format();
		}
		else if (o instanceOf Decimal)
		{
			return ((Decimal)o).format();
		}

		return o;
	}

	/**
	 * Inner class representing the Request
	 */
	private class Request
	{
		public String Username {get; set;}
	}

	/**
	 * Inner class representing the Response
	 */
	private class Response
	{
		public List<WorkItem> WorkItems {get; set;}

		public Response()
		{
			WorkItems = new List<WorkItem>();
		}
	}

	/**
	 * Inner class representing a Work Item
	 */
	private class WorkItem
	{
		public Id WorkItemId {get; set;}
		public Id ObjectId {get; set;}
		public String WorkItemName {get; set;}
		public String ObjectName {get; set;}
		public List<Field> Fields {get; set;}

		public WorkItem(Id workItemId, Id objectId, String objectName, String workItemName)
		{
			this.WorkItemId = workItemId;
			this.ObjectId = objectId;
			this.ObjectName = objectName;
			this.WorkItemName = workItemName;
			this.Fields = new List<Field>();
		}
	}

	/**
	 * Inner class representing a Field label and value
	 */
	private class Field
	{
		public String Name {get; set;}
		public String Value {get; set;}

		public Field(String name, String value)
		{
			this.Name = name;
			this.Value = value;
		}
	}

	/**
	 * Exception class
	 */
	private class GetWorkItemsException extends Exception {}
}

Approve Work Items REST Web Service

The second of our web services is to carry out the approval or rejection process on a single work item.

The request takes the Id of the work item and a boolean to indicate whether to approve or reject. The response simply returns a success indicator in the form of a boolean.

Here’s the code:

/**
 * Rest based service to Get Work Items
 */
@RestResource(urlMapping='/ApproveWorkItem/*')
global with sharing class ApproveWorkItemRESTService
{
	private static final String STATUS_APPROVED = 'Approved';
	private static final String STATUS_REJECTED = 'Rejected';

	/**
	 * Service Entry point
	 */
	@HttpPost
	global static void doPost()
	{
		RestRequest restReq = RestContext.request;
		RestResponse restRes = RestContext.response;

		// JSON parse the body into a Request object
		Request req = (Request)JSON.deserialize(restReq.requestBody.toString(), Request.class);

		// Process the Request
		Response res = processRequest(req);

		// Serialize the Response
		restRes.responseBody = Blob.valueOf(JSON.serialize(res));
	}

	/**
	 * Process the Request
	 */
	private static Response processRequest(Request req)
	{
		// Create a Process Work item Request
		Approval.ProcessWorkitemRequest processReq = new Approval.ProcessWorkitemRequest();

		processReq.setWorkitemId(req.WorkItemId);

		if (req.Approve)
		{
			processReq.setComments('Approved.');
			processReq.setAction('Approve');
		}
		else
		{
			processReq.setComments('Rejected.');
			processReq.setAction('Reject');
		}

		// Submit the request for approval
		Approval.ProcessResult processResult = Approval.process(processReq);

		// If it failed, capture the errors
		if (!processResult.isSuccess())
		{
			String errors = '';

			for (Database.Error err : processResult.getErrors())
			{
				errors += err.getMessage() + '/n';
			}

			throw new ApproveWorkItemException(errors);

		}

		return new Response(true);
	}

	/**
	 * Inner class representing the Request
	 */
	private class Request
	{
		public Id WorkItemId {get; set;}
		public Boolean Approve {get; set;}
	}

	/**
	 * Inner class representing the Response
	 */
	private class Response
	{
		public Boolean Success {get; set;}

		public Response(Boolean success)
		{
			this.Success = success;
		}
	}

	/**
	 * Exception class
	 */
	private class ApproveWorkItemException extends Exception {}
}

That’s all for now …

In the second part of this post I’ll show you the Pebble watch app and how to connect it to the REST based web services.

Tagged with: , , , ,
Posted in apex, code, force.com, pebble watch, salesforce
About Me
Product Services Developer at:
FinancialForce.com
All views expressed here are my own. More about me and contact details here.
Please sponsor me …
My wife and I are running the Great North Run to support The Nick Alexander Memorial Trust

If you've found my blog useful, please consider sponsoring us. Sponsor me on BT MyDonate

Enter your email address to follow this blog and receive notifications of new posts by email.

Copyright (there isn’t any, feel free to reuse!)

CC0
To the extent possible under law, Tony Scott has waived all copyright and related or neighboring rights to MeltedWires.com Examples and Code Samples. This work is published from: United Kingdom.

%d bloggers like this: