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

Productivity Tip : Quicker Javascript and CSS development using Dropbox

I’ve been working on a Javascript/CSS heavy Visualforce page this week and it can be a real pain tweaking the code, saving the library, zipping up the static resource, uploading it to Salesforce, testing the change and then repeating the process. Alternatively, I could put all my JS/CSS inside the VF page until I’m done but it still makes for time consuming saves up to the Salesforce cloud.

The following tip is a variation on a trick John Conners showed me, the difference being I’m using Dropbox as a temporary webserver instead of rolling my own.

A much quicker way of working is to take your JS and CSS libraries out of your static resource and put them in your public Dropbox folder while you’re working on them.

Once in your dropbox folder you can right click and get the public link:

Dropbox Public Link Example

Next simply comment out the references to your static resource and replace them with your dropbox public links …

<apex:page>
<!--	<apex:includeScript value="{!URLFOR($Resource.MyStaticResource, '/MyStaticResource/web/js/myjslib.js')}"/> -->
	<apex:includeScript value="https://dl.dropboxusercontent.com/u/26FG9442D4A/testscripts/myjslib.js" />
<!-- 	<apex:stylesheet value="{!URLFOR($Resource.MyStaticResource, '/MyStaticResource/web/css/mycss.css')}"/> -->
	<apex:stylesheet value="https://dl.dropboxusercontent.com/u/26FG9442D4A/testscripts/mycss.css" />

...

</apex:page>

You can then code away in your favourite editor and save your changes direct to your dropbox in seconds, reload your VF page and test your changes.

When you’re all set pop your scripts back, zip up your static resource and don’t forget to replace your temporary dropbox links with the static resource links.

Tagged with: , , , , ,
Posted in code, productivity
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: