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

Apex Code Tip – Sending Email to Salesforce Users

Email with WarningApex allows you send emails from code using the Messaging namespace. As usual Salesforce governor limits are at work and you could unwittingly hit your limits without needing to.

If you are sending email to users within your organisation, make sure you send the email to a User not an email address. Emails to Users are not included in this limit.

By way of example, consider a batch job that notifies the user submitting the job whether the job succeeded or failed:

CrossBad:
Avoid using an Email Address for a Salesforce user.

Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[] { UserInfo.getUserEmail() });
mail.setSubject('Batch Job Status Report');
mail.setPlainTextBody('The job completed successfully. Have an A1 day!');

tickBetter:
Use the Target Object Id to reference the Salesforce User.

Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setTargetObjectId(UserInfo.getUserId());
mail.setSaveAsActivity(false);
mail.setSubject('Batch Job Status Report');
mail.setPlainTextBody('The job completed successfully. Have an A1 day!');

Note, you need to use: setSaveAsActivity(false) when using a target object Id as the default value is true.

Remember you can get a User Id from the CreatedBy, LastModifiedBy or OwnerId fields on an object. However, be aware that the OwnerId could instead be a Queue.

More Info:

Email Governor Limits.

Messaging Namespace.

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

Undercover Maps

Eyes hiding behind a map.The Map class is probably the most useful collection on the Salesforce platform.

When you combine a Map with a SOQL query you have a really cool tool for automatically populutating a Map of SObjects keyed on Id using a special Map constructor:

Map<Id, Account> accountMap = new Map<Id, Account>([Select Id, Name From Account]);

This is well documented (and really useful for bulk loading foreign objects in triggers) but it turns out you can use this map constructor to populate a Map from any list of SObjects.

In the following example we have a query that loads Accounts as well as Opportunities. You can use the Map constructor to load the subquery results into a Map as follows:

// Query Accounts and Opportunities into a Map
Map<Id, Account> accountMap = new Map<Id, Account>([Select Id, Name, (Select Id, Name From Opportunities) From Account]);

// Iterate over the Map keys
for (Id key : accountMap.keySet())
{
    Account acc = accountMap.get(key);

    System.debug('Account: ' + key + ' => ' + acc.Name);

    // Load the Opportunities into a Map
    Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>(acc.Opportunities);

    // Iterate over the Map Keys
    for (Id key2 : oppMap.keySet())
    {
        Opportunity opp = oppMap.get(key2);

        System.debug('Opportunity: ' + key2 + ' => ' + opp.Name);
    }
}

You can do the same with Dynamic SOQL with one subtle difference, you need to check for Nulls:

// Query to execute
String query = 'Select Id, Name, (Select Id, Name From Opportunities) From Account';

// Query Accounts and Opportunities into a Map
Map<Id, SObject> sObjectMap = new Map<Id, SObject>(Database.query(query));

// Iterate over the Map keys
for (Id key : sObjectMap.keySet())
{
    SObject so = sObjectMap.get(key);

    System.debug('Account SObject: ' + key + ' => ' + so.get('Name'));

    // Load the Opportunities into a Map
    List<SObject> sos = so.getSObjects('Opportunities');

    // Check the Map for Null in case the Account has no Opportunities
    if (sos != null)
    {
        Map<Id, SObject> soMap2 = new Map<Id, SObject>();

		// Iterate over the Map keys
        for (Id key2 : soMap2.keySet())
        {
            SObject so2 = soMap2.get(key2);

            System.debug('Opportunity SObject: ' + key2 + ' => ' + so2.get('Name'));
        }
    }
}

The Map constructor also allows you to use a Dynamic SOQL query to load into Map of concrete SOBjects by casting it to a List:

String query = 'Select Id, Name From Account';

Map<Id, Account> accountMap = new Map<Id, Account>((List<Account>)Database.query(query));

A Final Word of Warning:

Always exercise caution when using undocumented features within Salesforce. They may break without warning.

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

Routine

This piece was written for my first Leeds Savage Club meet on the theme ‘Routine’. A jolly nice bunch of people they are too.

Another morning in the Kent household. Forget the alarm clock, my body clock is accurate to a nanosecond. 7.30 sharp, my eyes open and I slip silently out from under the Spiderman duvet leaving Lois to continue her journey to Morning Town undisturbed. She’s got a day off and a rude awakening is one piece of news I don’t want to write about. It’ll take more than nerves of steel to face waking that particular grisly bear on a day off. I creep out to the bathroom then hit the shower. Eight seconds later I’m washed and stood in front of the bathroom mirror, I don’t mess about. I have to wait a minute for the steam to evaporate so a quick on the spot cyclone spin and I’m dried off. I return to focus my gaze upon my reflection in the mirror. You may think there is a touch of vanity going on here but I can assure you not. I narrow my eyes, calculate the correct angle of trajectory and with pinpoint precision I focus my laser beam vision onto the mirror surface and let it reflect back upon my face to clear the nights build up of stubble. Who needs to mess around with shaving foam and razors? There’s no shortcut for my gleaming white incisors but I can polish a full set till they sparkle in less than four seconds flat. No one wants to be rescued by a hunk with bad teeth. The hair takes a little longer and a smidgen of wax just to get that forelock to sit right. Looking good, I grab my robe from the hook on the door taking the hook with it. I forget my own strength sometimes. I’ll need to fill and repair that when I get home. I kid myself, and hope Lois doesn’t notice. I put on my robe, neatly monikered with the letters CK, and head to my closet.It’s quite a large closet, for a guy, with two big open out compartment doors. On the left hand side I keep my suits and shirts. They’re all hung up neatly, buttoned up, pressed and starched. A selection of ties hang on a rail from behind the door. Next to these on a little rack are six identical pairs of my signature black rimmed spectacles. I’m telling you, you wouldn’t believe how quickly I go through a pair of specs, my dispensing optician loves me. At the bottom lie a neat row of polished black brogues. To the right of the closet hang my special suits, you know, the famous blue and red numbers complete with matching capes. I usually keep two on standby and one at the cleaners. I tell ya too, they’re not the most comfortable set of threads in the world. I’ve put on a pound or two since I met Lois and despite the ‘one size fits all’ label they still feel a bit on the tight side. Worse still when you have to keep them on underneath your day clothes you need a super human bladder. Believe me, you do not want to get caught short with a tight fitting costume on beneath your civvies, that’s for sure! A little dose of talc is in order too before I stretch one on, just to stop it chaffing. Once I’m suited up I have to lower my body temperature by two degrees or it becomes unbearable! I glance over toward the bedroom wall and with my x-ray vision and check that Lois is still sleeping soundly. She’s still exhaling the zeds. Sometimes she can sleep through the sound of a bomb exploding on her day off. That’s not a joke by the way, it happened two Tuesdays ago a couple of blocks down. I’d heard the ticking before it went off and flew out of the window with mere seconds to spare, shielded the blast with my cape, rescued a granny and her cat on the ninety-sixth floor and returned to find Lois still slumbering. I envy that girl at times, a grenade pin drops three miles away and I’m awake in an instant.

I finish getting dressed then make my way to the kitchen and throw two slices of sour dough in the air over a plate. Before they touch down I give them a light toasting with a quick laser glance and reach for the butter and marmalade. There’s still a cupful of coffee in the pot from last night so another brief stare and it’s steaming hot. I detect a feint rattle in the lobby of our apartment block, despite us being on the sixty eighth floor, it must be the paperboy. I quietly sneak out the front door and rush down the stairs. I spot Mr. Jones from number twenty one on his way up but don’t stop to chat. To him there’s a sudden gust of wind down the stairwell and he doesn’t even register me. Six seconds later I’m back in the kitchen, newspaper in hand. I pull up to the breakfast bar and see that Lois got a front page spread on yesterdays shark attack rescue down on pier eighteen. Jimmy got a good action shot in too. I even managed a diamond white smile for the camera as I forced open the sharks jaws with my bare hands releasing its vice like grip on the flailing fisherman. It’s a good piece by Lois, it still amazes the chief that Lois is the only hack on the rag who can manage to get a direct quote from the man of steel himself before he makes a sharp exit. Little does he know!

I check my watch out of habit, I’m running a little late. Still, I can’t hang around there’s another headline to write and a world to keep safe. I creep back to the bedroom, plant a gentle kiss on Lois’s forehead so as not to wake her before returning to the kitchen. The subway is going to be jammed at this hour so I tear off my suit and shirt, roll them up into my briefcase then clamber out of the window and soar off to work. There’s a telephone booth on a quiet side street near sixth avenue, I’ll get changed there.


For more words, please visit my Writing Blog

Creative Commons License
‘Routine’ by Tony Scott is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.

Posted in creative writing

Apex Approval Actions

Approval Processes are a great feature of the Salesforce platform. They stand firmly behind the ‘No Software’ banner by giving ‘Power Users’ the ability to configure approval criteria and outcomes via a point and click interface. Aproval outcomes can create tasks, update fields, send emails or even send outbound messages to another system. What’s not to like? One drawback, however, is that quite often you’ll find that Approval Processes are created and maintained by ‘Power Users’ rather than developers. ‘Power Users’ often have an intimate knowledge of the business processes but little or no knowledge of the code base. So, when an Approval Process requires a more complex outcome it may be that a developer needs to get involved and the ‘No Software’ premise falls apart. When working on the Salesforce platform in cases like this I prefer to think about how better to empower users. In the case of Approval Processes one way of providing this power is to offer them a suite of approval actions that they can easily plug into their own process flows. To achieve this, I’m going to show you a trigger based pattern that uses a custom picklist field to invoke a custom action. When a user is creating their Approval Process (or even a workflow for that matter!) they can use a field update to run a specific action against the object. If you’re working in a managed package this approach is extensible allowing you to package the framework along with a standard suite of Approval Actions and then let your customers implement their own. For this example let’s consider a couple of useful actions we may want to perform when approving or rejecting an Opportunity. If an Opportunity is approved we may want to record a Sale against the Opportunity owners sales target. If an Opportunity is rejected we may want to add the Opportunity to a list of items that require further review at the next weekly sales meeting. The list could go on, you could raise an invoice, produce and email a statement of works etc etc. We’ll begin this approach by defining a global interface that all Approval Action classes must implement.

/**
 * Global Interface for Approval Actions
 */
global Interface IApprovalAction
{
	/**
	 * Execute method.
	 *
	 * Arguments:	SObject[] - Array of SObjects to process
	 *		String - Field containing the action class name
	 */
	void execute(SObject[] sobs, String classLocatorField);

	/**
	 * Validate Type
	 *
	 * Arguments: 	SObjectType - The type of the Object being passed
	 *
	 * Returns:	Boolean - true if the supplied type is correct
	 */
	Boolean validateType(SObjectType soType);
}

The interface contains two methods. The execute() method takes a payload of SObjects to perform an action against. Bear in mind this logic is being handled in the context of a trigger and like all trigger code it should be designed to handle bulk operations. For that reason our interface accepts a list of SObjects rather than a single SObject even though approvals tend to work on a single record at a time. There’s nothing to prevent this pattern being called in other trigger contexts so for safety we’ll keep that in mind. The validateType() method is used to ensure the action is compatible with the object passed to it. We’re going to use a Picklist field with all the available approval action class names on our target object, in this case the Opportunity object. Opportunity Custom Field Now class names aren’t always very user friendly so consider the names carefully. Be sure to add the namespace prefixes to them if applicable. Tip: You can localise picklist values if you want to make it super clear as long as the actual value matches your class names. In our example we need two approval action classes, one called RecordSaleAction and one called AddToReviewListAction. The RecordSaleAction class implements the interface and adds a record to the Sale__c object:

/**
 * Approval action for recording a sales against an Opportunity
 */
public without sharing class RecordSaleAction
	implements IApprovalAction
{
	/**
	 * Constructor
	 */
	public RecordSaleAction()
	{
	}

	/**
	 * Execute method (defined by interface)
	 *
	 * This method should be bulkified since it deals with an array of SObjects.
	 *
	 * Arguments:	SObject[] - Array of SObjects to process
	 *		String - Field containing the action class name (which needs to be cleared to prevent actions refiring)
	 */
	public void execute(SObject[] sobs, String classLocatorField)
	{
		// List of Sales
		List<Sale__c> sales = new List<Sale__c>();

		// Loop through the SObjects
		for (SObject so : sobs)
		{
			// cast to a concrete type
			Opportunity opp = (Opportunity)so;

			// add a new Sale to the list
			sales.add(new Sale__c(
				Opportunity__c = opp.Id,
				Salesman__c = opp.OwnerId,
				SaleValue__c = opp.Amount
			));

			// reset the approval action field
			opp.put(classLocatorField, null);
		}

		// insert the Sale__c records and update the Opportunities
		insert sales;
		update sobs;
	}

	/**
	 * Validate the SObject type this action supports.
	 *
	 * Arguments: 	SObjectType - The type of the Object being passed
	 *
	 * Returns:	Boolean - true if the supplied type is correct
	 */
	public Boolean validateType(SObjectType soType)
	{
		// Must be an Opportunity
		if (soType == Opportunity.SObjectType)
		{
			return true;
		}

		return false;
	}
}

The Sale__c object looks like this:

Field Label Type Properties
Id Record ID id
Name Sales Id text(80)
SaleValue__c Sale Value currency(16,2)
Salesman__c Salesman foreign key User
Opportunity__c Opportunity foreign key Opportunity

Similarly the AddToReviewListAction class also implements the interface but adds a record to the PendingReview__c object:

/**
 * Approval action for adding an Opportunity to the review list
 */
public without sharing class AddToReviewListAction
	implements IApprovalAction
{
	/**
	 * Constructor
	 */
	public AddToReviewListAction()
	{
	}

	/**
	 * Execute method (defined by interface)
	 *
	 * This method should be bulkified since it deals with an array of SObjects.
	 *
	 * Arguments:	SObject[] - Array of SObjects to process
	 *		String - Field containing the action class name (which needs to be cleared to prevent actions refiring)
	 */
	public void execute(SObject[] sobs, String classLocatorField)
	{
		// List of Reviews
		List<PendingReview__c> reviews = new List<PendingReview__c>();

		// Loop through the SObjects
		for (SObject so : sobs)
		{
			// cast to a concrete type
			Opportunity opp = (Opportunity)so;

			// add a new PendingReview to the list
			reviews.add(new PendingReview__c(
				Opportunity__c = opp.Id
			));

			// reset the approval action field
			opp.put(classLocatorField, null);
		}

		// insert the PendingReview__c records and update the Opportunities
		insert reviews;
		update sobs;
	}

	/**
	 * Validate the SObject type this action supports.
	 *
	 * Arguments: 	SObjectType - The type of the Object being passed
	 *
	 * Returns:	Boolean - true if the supplied type is correct
	 */
	public Boolean validateType(SObjectType soType)
	{
		// Must be an Opportunity
		if (soType == Opportunity.SObjectType)
		{
			return true;
		}

		return false;
	}
}

The PendingReview__c object looks like this:

Field Label Type Properties
Id Record ID id
Name Review Id text(80)
Opportunity__c Opportunity foreign key Opportunity

Note that the Action classes should ensure they reset the Action field to null on completion so that another update against the record does not cause the action to be fired again. I could have added this logic to the handler but I wanted to keep it flexible enough so that if needed the Action could execute asynchronous code if required. To hook up the Action classes to the triggers we have a generic handler class that sorts the appropriate actions to execute and delegates them accordingly. As we are dealing with a trigger the handler needs to be able to handle a bulk operation not just one record at a time. The handler looks like this:

/**
 * Factory class for resolving and executing Approval Actions
 */
global without sharing class ApprovalActionHandler
{
	/**
	 * Static handler method to execute the appropriate action
	 * for the given SObjects.
	 *
	 * Arguments:	String - Class locator field to determine the class to call.
	 */
	global static void handle(String classLocatorField)
	{
		// This handler should only be executed in an after update trigger context
		if (!(Trigger.isUpdate && Trigger.isAfter))
		{
			return;
		}

		// if no locator provided, quit now
		if (classLocatorField == null ||
			classLocatorField == '')
		{
			return;
		}

		// Get the objects to process from the Trigger context
		SObject[] sobs = Trigger.new;

		// Create a map of Class Name to Action
		Map<String, Action> actionMap = new Map<String, Action>();

		// iterate the sobjects, populating the map
		for (SObject so : sobs)
		{
			// get the class name from the class locator field
			String className = (String)so.get(classLocatorField);

			// if we have a class set
			if (className != null && className != '')
			{
				// get the action from the map
				Action myAction = actionMap.get(className);

				// if there is no action, construct it and add it to the map
				if (myAction == null)
				{
					IApprovalAction implementation = actionResolver(className);

					myAction = new Action(implementation);
					actionMap.put(className, myAction);
				}

				// validate the object is suitable for the implementation
				if (!myAction.Implementation.validateType(so.getSObjectType()))
				{
					throw new HandlerException('Action class: ' + className + ' does not support Object Type: ' + so.getSObjectType());
				}

				// store a clone of the object in the payload (this is because we are in the after trigger and the object is now read only)
				myAction.Payload.add(so.clone(true, true));
			}
		}

		// loop through the action map values and execute the actions
		for (Action myAction : actionMap.values())
		{
			// execute the action
			myAction.execute(classLocatorField);
		}
	}

	/**
	 * Action resolver method.
	 *
	 * Arguments:	String - Class name to resolve
	 *
	 * Returns:	IApprovalAction
	 */
	private static IApprovalAction actionResolver(String className)
	{
		Type t = Type.forName(className);

		// We can't find a type, throw an exception
		if (t == null)
		{
			throw new HandlerException('Could not resolve action: ' + className);
		}

		// Instantiate the type
		Object o = t.newInstance();

		// if its not an instance of IApprovalAction, throw an exception
		if (!(o instanceOf IApprovalAction))
		{
			throw new HandlerException('Class: ' + className + ' does not implement IApprovalAction.');
		}

		return (IApprovalAction)o;
	}

	/**
	 * Inner class to wrap an actions implementation and payload.
	 */
	private class Action
	{
		public IApprovalAction Implementation {get; set;}
		public List<SObject> Payload {get; set;}

		public Action(IApprovalAction implementation)
		{
			this.Implementation = implementation;
			this.Payload = new List<SObject>();
		}

		public void execute(String classLocatorField)
		{
			Implementation.execute(Payload, classLocatorField);
		}
	}

	// Exception class
	global class HandlerException extends Exception {}
}

You’ll notice that the SObjects are cloned before being added to the Action classes payload. This is because our trigger is operating on the After Update so as not to intefere with any other trigger processing. At this point in the trigger the SObject is read only. Cloning it effectively releases it from its read only state and allows the Action class to fire a second update in order to clear its action state once the action has been completed.

To complete the code we add a simple trigger to the Opportunity. This is to invoke the handler and pass in the API name of the picklist containing the Action class to invoke:

/**
 * Trigger to invoke the Approval Actions
 */
trigger OpportunityApprovalActionTrigger on Opportunity (after update)
{
	ApprovalActionHandler.handle('ApprovalAction__c');
}

Ok, so all that remains is to throw it into the hands our ‘Power Users’ to add it into their Approval Processes then it’s job done: Approval Process

Field Update 1

Field Update 2

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

Life in the Slow Lane – Summer Vacation 2013

Orange VW T2 camper van

It’s been a little bit of a pipe dream of mine to take off behind the wheel of an old VW camper van. A couple of years ago a friend posted some pictures of their weekend travels through Yorkshire in an orange Type 2. I enquired where they got the van from and they pointed me in the direction of Liberty Campers.

I decided to take the plunge for this years summer holiday with my nine year old daughter and take off in a T2 to Wales. I contacted John at Liberty Campers and booked ‘Dougal’, the very same camper my friend hired, for the trip. John explained that Dougal was a newer Type 2 with a water cooled engine that tend to be more reliable on a longer trip. Don’t be fooled into thinking it’s a modern vehicle, it’s not. Other than the engine nothing has really changed and you are still driving a vehicle that made it’s debut in the late 1960’s. As John pointed out motorways hadn’t been invented when the VW camper van was first conceived.

On the day of departure my daughter and I drove over to meet John and Dougal in Ilkley where they are based. John is obviously an enthusiast. He maintains and restores his own vans and is keen that people get a sense of what heading out in a Type 2 is all about. It’s not about caning it down the motorway, a comfortable cruising speed is around 40 or 50 mph. It’s about slowing things down, finding the back roads, taking in the scenery and enjoying the ride. John showed us round the van and explained the equipment. Dougal comes with a pop-up roof where my daughter excitedly wanted to sleep, a small fridge, a two ring gas stove and grill, sink with running water, CD player with an iPod/Aux connector, an electric hookup, kettle, heater, cutlery and cooking equipment. The back seat folds out to make a bed for the grown ups. The fridge, water pump and interior lights work off a separate 12 volt leisure battery that is charged as you drive so you don’t need to worry if your campsite doesn’t have an electric hookup. You can also charge your phone off it! On our 10 day trip we only managed to hookup on two nights and it wasn’t a problem at all.

After our introduction we set off on our adventure to Wales. Driving takes a little getting used to but after a few miles you start to get familiar with it then it’s big grins all the way. You don’t have the luxury of power steering but there is something satisfying about turning the big old steering wheel. The gearbox is the same after you get used to it. It has four forward gears and a long gear stick and the gears pop in with a gratifying clunk. The handbrake is on a ratchet in the dashboard just above the gear lever which takes a bit of getting used to as well. Once you are used to it, however, it’s a pleasure to drive as you bimble along twisting winding roads waving at other VW campers coming the other way.

Allow a lot of extra time for your journey as you’ll want to pull up at the side of the road or stop off at a picnic spot and put the kettle on and wonder why everyone else is in such a rush. In a camper van it really is about the journey not the destination. We took Dougal down through Welshpool to Cardigan then up through Snowdonia and back along North Wales. Parked up near the beach or driving along coastal roads and mountain passes it was sheer pleasure and relaxation. The van was a real talking point and even my daughter was bringing her campsite friends over to have a look at it. During the day we’d drive out to the beach or head off to the local sights or just go for a drive. On an evening it was nice just sit to sit in the van with the sliding door open with a book and a glass of wine watching the sun go down while my daughter ran off with her new found friends. On quieter evenings we’d sit in the van and play with the travel board games we’d brought. There was no need for television or computer games.

I know this review seems somewhat biased but it’s hard to fault taking to the road in a piece of 60’s nostalgia. If you are expecting all the comforts of home you are missing the point somewhat. This is all about leaving modern life behind, winding down and taking your time and we both loved it.

Links: Liberty Campers

Posted in travel

Conditional Detail Page Buttons – Hacking the Platform

UPDATE: Due to changes in the way Salesforce serves up home page components from a different domain, this will no longer work as javascript no longer has access to the DOM.

Introduction

I’m going to start this post off with a BIG RED CAVEAT:

keyboard

This makes use of undocumented techniques on the Salesforce platform. This is risky business as platform behaviour may change without notice or warning leading to broken code. You have been warned!

Ok, so the warning aside, I’m going to show you how to ‘hack’ the platform to make those Salesforce detail page buttons render according to rules on the target record.

The motive for this exercise was a conversation I once had with a client who wanted to know if it was easy to disable the buttons based on some criteria on the record to which I replied, ‘No’. I decided at some point I’d have a play around and see if I could come up with a solution that didn’t involve having to write Visualforce pages to replace the standard detail pages by way of a challenge if nothing else. This is the fruit of that which you are free to play around with but only if you heed my caution! 😉

I think Salesforce are missing a trick by not including this behaviour as standard. An ideas posting me thinks.

The Approach

There are 2 parts to this. One is the logic that determines whether a button should be hidden or not and the other is the actual hiding of the button.

The show/hide logic

This is the simpler part. I decided the easiest way was to create a Custom Object that allows a User or Sys Admin to define which buttons should be hidden by binding the button name to a Checkbox field on the object. If the checkbox is checked then the button should be hidden. Using a checkbox field on the object rather than writing some complex condition parser means the user can simply add a Checkbox formula field and leverage the platforms own condition parser.

The Condition Set custom object below:

Screen Shot 2013-07-28 at 15.45.32It’s a master detail. The header record refers to the SObject API name that we want to add conditions for. The key prefix is the 3 character Id that you can use to determine the object type from an Id. I’ve written trigger to populate this so you only need enter the SObjects API name. I’ve added a few standard button hide fields as well a picklist option to decide whether to Hide or Disable the buttons. For the Standard buttons you need to specify the API name of a checkbox field on the object to bind to. This checkbox must resolve to TRUE in order for the associated button to be hidden. For custom buttons (and other standard buttons) you can add Custom Conditions via the related list. In this case you need to specify the API Name of the Button and the API name of the check box to bind to. There’s no reason why you can’t bind more than one button to the same checkbox either.

The Hiding of the Buttons

This is where a bit of platform black magic comes in. I’ve created a Javascript Homepage Component that you can add to the standard sidebar.

Screen Shot 2013-07-28 at 16.01.36

Using the Chrome debugging console I  worked out the name attribute of the standard buttons, eg. edit, delete, submit, clone. I also found that any custom buttons have a name attribute that matches the API name albeit in lower case. On a detail page the Id of the object being viewed can been obtained from a property pinned on the window object: window.sfdcPage.entityId

So armed with this information I created a Web Service that takes an SObject Id and resolves that to a Condition Set using the SObjects Key Prefix. (The three character code that identifies an SObjects type.) It then queries the SObject itself to determine which buttons to hide using the bound fields from the Condition Set.

The homepage component contains the Javascript that uses the Ajax Toolkit to call the web service and retrieve the list of buttons to hide. If the component is able to locate a Condition Set then the buttons are hidden or disabled with a bit of jQuery. The component itself is hidden so as not to consume any screen real estate.

One complication arose using the Ajax Toolkit which was where to get the session token from as the Home Page Components don’t have access to any merge fields. I found a trick that Ron Hess had posted up on the Salesforce boards that extracts the Session Id from the page cookie to get round this.

An Account Detail Page with Disabled Buttons

So to show you it in action here’s the Account Detail Page using the above Condition Set:

Screen Shot 2013-07-28 at 16.28.02

Cool, now can I see the Code?

To make life easier I’ve uploaded it all into an unmanaged package that you can get here: http://j.mp/1c2R5FF

The package consists of:

  • A Trigger: ConditionSetTrigger.trigger – The trigger uses the describe information for the entered SObject API Name to populate the 3 character Key Prefix.
  • A Web Service: ConditionSetWebService.cls – The service provides a method that takes an SObject Id and returns the hide button data used by the Home Page Component.
  • A Home Page Component: Conditional Butons – This is the javascript that hides the buttons.
  • A Test Class: ConditionSetTests – A couple of unit tests for the service and the trigger.
  • 2 Custom objects: Condition Set / Custom Condition – Provide the Condition Set data.
  • A Tab: Condition Set – Tab for defining the Condition Sets.

Installation Steps:

  • Install the package.
  • Add the Home Page Component to the Home Page Layout. (Setup -> Customize -> Home -> Homepage Layouts)
  • Make sure the Sidebar always shows custom components.  (Setup -> Customize -> User Interface -> Show Custom Sidebar Components on All Pages)
  • Create any Custom Checkbox Fields of Checkbox Formula Fields on the SObjects whose buttons you want to control.
  • Create the Condition Sets and bind the Hide Fields to the Checkbox fields on your SObject. Note, when entering API names they must match exactly the API names in your org with namespace prefixes as appropriate.
  • Be aware that it uses Google hosted libraries for the jQuery library. Make sure this content is not blocked: //ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js

The homepage component code is a bit compressed and unreadable so I’ve provided it here as well in a more tabulated form:

<script src="/soap/ajax/28.0/connection.js"></script>
<script src="/soap/ajax/28.0/apex.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
		/* Document Ready */
		$(document).ready(function() {
			/* Hide the Sidebar component */
			$('#ConditionalButtonsComponent').closest('.sidebarModule').hide();
		});

		/* Window Loaded */
		$(window).load(function() {
			/* Namespaced Javascript functions */
			var TS =
			{
				hideButtons: function()
				{
					/* Get the object Id of the detail page */
					var objectId = window.sfdcPage.entityId;

					/* If we have an object id, we can pass it to the webservice */
					if (typeof objectId != 'undefined')
					{
						/* Extract the Session Id from the page cookie */
						var sid = TS.readCookie('sid');
						sforce.connection.sessionId = sid;

						/* Make an asynchronus call to the webservice */
						sforce.apex.execute('ConditionSetWebService',
											'getHiddenButtons',
											{Id: objectId},
											{
												onSuccess: TS.doHide,
												onFailure: function(error) { TS.logToConsole(error); }
											});
					}
				},

				doHide: function(response)
				{
					var responseObject = JSON.parse(response);

					for (var i = 0; i < responseObject.HiddenButtons.length; i++)
					{
						var button = responseObject.HiddenButtons[i].toLowerCase();

						if (responseObject.HideOption == 'Disable')
						{
							$('input[name="' + button + '"]').attr('disabled', 'disabled').fadeTo('fast', 0.4);
						}
						else
						{
							$('input[name="' + button + '"]').hide();
						}

					}
				},

				/**
				 * readCookie function by Ron Hess
				 */
				readCookie: function(name)
				{
					var nameEQ = name + "=";

					var ca = document.cookie.split(';');

					for(var i = 0; i < ca.length; i++)
					{
						var c = ca[i];

						while (c.charAt(0) == ' ')
						{
							c = c.substring(1, c.length);
						}

						if (c.indexOf(nameEQ) == 0)
						{
							return c.substring(nameEQ.length, c.length);
						}
					}

					return null;
				},

				/**
				 * Chrome only
				 */
				logToConsole: function(message)
				{
					if(typeof console == "object")
					{
						console.log(message);
					}
				}
			};

			/* Hide the buttons */
			TS.hideButtons();
		});
</script>
<span id='ConditionalButtonsComponent'>Conditional Buttons</span>
Posted in apex, code, force.com, 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: