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.

Advertisements

Author, brainstormer, coder, dad, explorer, four chord trickster, gig goer, home worker, inquisitor, joker, knowledge seeker, likes: marmite, note scribbler, opinionator, poet, quite likes converse, roller skater, six music listener, tea drinker, urban dweller, vinyl spinner, word wrangler, x-factor hater, Yorkshireman (honorary), zombie slayer (lie).

Tagged with: , , , ,
Posted in apex, code, force.com, pebble watch, salesforce
One comment on “Pebble Watch: Salesforce Approvals – Part 1
  1. Great Post Tony! You are awesome!

Comments are closed.

About Me
Product Services Developer at:
FinancialForce.com
All views expressed here are my own. More about me and contact details here.

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: