Skip navigation

Tag Archives: visualforce

One of the most hyped, and in my opinion least documented features of the Summer 13 release of Salesforce is custom Chatter Publisher Actions. Various Salesforce bloggers have written about how easy it is to create and use regular Chatter Publisher Actions, which, basically, just let you quickly create records related to a primary record from your Chatter Feed (e.g. from an Account’s Chatter feed, creating a Contact or Opportunity, without having to go down to the corresponding related lists. For one excellent discussion of regular Publisher Actions, see Daniel Hoechst’s post. The release notes also give examples of regular Publisher Actions, so I won’t go into any more depth on them here.

However, neither the release notes, nor any other bloggers, nor Salesforce themselves in their recent release webinars, have yet shown any working examples of custom Publisher Actions. What are they? Here’s a brief overview:

  • They come in two flavors: Global and Object-Specific.
    • Global custom Actions: can be added to any Chatter Publisher Layout, anywhere.
    • Object-specific custom Actions: can only be added to one specific object’s Publisher Layouts.
  • You create them using Visualforce Pages.
    • For Global custom actions, your Visualforce page must either have no controller, or a custom controller that is NOT an extension.
    • For Object-specific custom Actions, your Visualforce page must use the standard controller of the object you’d like to use the Action with.
  • Chatter implements custom Actions using iFrames.
    • When creating a new custom Action, you must specify a height for your Action — this is setting the height of the iFrame in which your page will be included.
    • This iFrame is always run from within the Visualforce Page’s domain. So, if it’s a local VF Page, the domain will be something like “c.na14.visual.force.com”, whereas if the page is included in a managed package, the domain will be something like “skuid.cs15.visual.force.com”.

So custom Publisher Actions are, by virtue of having to be created through Visualforce Pages, and being embedded in iFrames, already more complicated to develop. This begs the question: what sort of “Action” would be compelling enough to warrant going through this hassle? What’s a good use case?

A use case: create a Contact and linked Opportunity Contact Role all at once, from an Opportunity

Many of the examples I thought of all fall into one category: creating records of multiple objects all at once, in particular, creating a junction object and creating the joined record if it doesn’t exist yet. For example, from an Opportunity record, you often want to create a new Opportunity Contact Role, but can’t because the related Contact doesn’t exist yet. Or, in a hypothetical iTunes-on-Force.com app, from an Album’s page, create a “Genre Association” junction object as well as a new Genre if it doesn’t exist yet.

I finally settled on the “Create a Contact and Opportunity Contact Role all at once” example.

Making it happen

The first step in creating a Custom Publisher Action is to create the Action’s associated Visualforce Page. For this example, my Custom Action is specific to the Opportunity object, so our Page must use the Opportunity Standard Controller. As for the body of the page, since I’m using Skuid, I just need to specify the name of a Skuid Page I want to include, which we’ll build later.

VisualforcePage_ForFeedAction

Now that we have a Visualforce Page associated to the Opportunity standard controller, we can create our Action!

The first step is to go to the newly-reorganized Opportunity “Buttons, Links, and Actions” section. In Summer 13, Salesforce has consolidated Custom Buttons, Custom Links, Standard Actions and Overrides (e.g. the place where you can override what happens for the “View”, “Edit”, “New”, and “Tab” action for each object), and the new Chatter Publisher Actions, into this one list for each Object.

Click on “New Action”, and then enter the following:

ButtonsLinksAndActions NewAction

Notice two things:

  1. The “Height” attribute corresponds directly to the height of the iFrame which will embed your Visualforce page
  2. The “Label” is the actual label that users will see for your new Publisher Action, e.g. they’ll see “Link”, “File”, “Post”, “Poll”, and “Add Contact”.

Now that we’ve created the action, we need to actually add it to one of our Opportunity page layout’s Publisher Actions area. By default, all objects will inherit from the default “Global” Publisher Action area, so we’ll need to explicitly “break” this inheritance to allow the set of Publisher Actions that are displayed on our Opportunity layout(s) to be different than the Publisher Actions for all other objects. TO do this, click “override the global layout” in the “Publisher Actions” area of one of your Opportunity page layouts.

OverrideTheGlobalLayout

Next, drag in the “Add Contact” Publisher Action we just created, and position it as you’d like relative to the other Publisher Actions:

DragInNewAction

Once we save the Page Layout, we’re ready to roll! This Publisher Action should show up in our Opportunity page’s Chatter feed. Because we positioned it last in the list, and there’s more than 4 Publisher Actions, our Action shows up under the “More” area:

SelectingThePublisherAction

Now, we’ll show how we built this with Skuid later, but here’s what our custom Publisher Action actually looks like:

PublisherAction_InAction

We’re presented with two columns of data. In the first column, we enter some bare bones info for a new Contact record we want to create that’s associated with this Opportunity’s Account. In the second column, we define the Opportunity Contact Role. Once we click Save, what Skuid will do is the following:

  1. Create a new Contact record using the fields we filled out, with the AccountId of the Opportunity in context (“Acme”)
  2. Create a new Opportunity Contact Role record, using the fields we entered, and associated to the Opportunity in context (“Acme”) and our newly created Contact (“Bilbo Baggins”)
  3. Create a new post in this Opportunity’s Chatter Feed describing what just happened.
  4. Refreshing the page — we’ll describe why this is necessary in a second.

In the Skuid page I built in place of the standard Opportunity detail page, I display Opportunity Contact Roles in a “Key People” tab. As you can see, a new “Bilbo Baggins” contact record was created, and an Opportunity Contact Role record linking him to the “Acme – 500 Widgets” Opportunity was created as well.

ContactRole_CreatedByFeedAction

Finally, a new FeedItem was created that records the addition of the new Contact and his Role:

FeedItem_CreatedByAction

The Skuid page that made the real magic happen

Getting the actual Publisher Action configured is pretty quick once you’ve got a Visualforce Page on hand to use. But it’s this Visualforce Page, of course, which has to do all the hard work to make the custom Publisher Action useful.

Using Skuid as the content of the Publisher Action made the process very fast and fairly painless. In a nutshell, here’s what I did:

(1) Create a new Skuid Page using the “New Page” template on the Contact object.

The “New Page” template, here, scaffolds a basic Skuid Page for use in creating brand new Contact records. It sets up a Model on the Contact page with “Create Default Row if None Exists” pre-checked, and adds a Field Editor component with the FirstName and LastName fields already thrown in:

NewSkuidPage

ContactModelProperties

(2) Add in 3 additional Models: on the Opportunity, OpportunityContactRole, and FeedItem objects:

AddlModels

The Opportunity Model grabs the Opportunity record in context from the page, using the “id” URL Parameter. We then grab the AccountId lookup field from the Opportunity, so that we can use this to automatically prepopulate the AccountId of our newly-created Contact:

OppConditions OppFields

For the Opportunity Contact Role model, we pull in the Role and IsPrimary fields so that we can display them in the page, and we add 2 “Model Merge” Conditions to associate the new record to the context Opportunity and the newly-created Contact:

OppCRConditions

Finally, for the FeedItem model, we add a single condition to automatically associate the new FeedItem to the Opportunity in context, and we add in the Body field, which we will edit in our Save action:

FeedItemConditions FeedItemFields

(3) Place 2 Field Editors, one each for the Contact and ContactRole Models, in separate Panels:

PanelSetLayout

(4) Embed this Panelset within the first step of a new Wizard Component

-We could have used a PageTitle component instead, but we do this because it allows for easy transition into multi-step wizards. We could easily have put the Opportunity Contact Role fields within a separate step of the wizard, and had a “Next Step”/”Previous Step” sequential navigation here.

Wizard

(5) Create a custom Inline (Snippet) to use for our “Save All” Wizard Step action

Skuid’s standard wizard action types almost suffice here for achieving everything we want to do, but we want to automatically populate the Body field of our new FeedItem, which takes a little code, and,  because we’re using this Page as a Chatter Publisher Action, which is run in an iFrame, we run in to a Gotcha involving using the URL Redirect action, as we need to reload the location of our frame’s parent, not the frame itself (window).

First, we customize our Action to execute a Snippet called Save All:

SaveAllStepAction

Finally, we create the Inline (Snippet) itself:

InlineSnippet

//
// Get our Models
var contactModel = skuid.model.getModel('ContactData'),
    contactRoleModel = skuid.model.getModel('ContactRole'),
    feedItemModel = skuid.model.getModel('FeedItem');

// Get the first rows in each of our Models
var contact = contactModel.getFirstRow(),
    contactRole = contactRoleModel.getFirstRow(),
    feedItem = feedItemModel.getFirstRow();

var bodyText = 'Added new ' + contactRole.Role
    + ' to this Opportunity: '
    + contact.FirstName + ' ' + contact.LastName
    + ' (' + contact.Title + ').';

// Update the Body of our new Feed Item
feedItemModel.updateRow(feedItem,'Body',bodyText);

// Save our Models,
// and when we're done,
// refresh the iframe's parent

skuid.model.save(
    [contactModel,contactRoleModel,feedItemModel]
,{ callback: function() {
    parent.location.reload();
}});

In the Snippet, we do the following:

  1. Get references to 3 of our Models
  2. Get references to their first rows
  3. Populate the “Body” field of the FeedItem model’s first row with the Role field from our OpportunityContactRole record and the First and Last Name fields from our Contact record
  4. Save all 3 of our Models in sequence, starting with the Contact model (VERY important) so that the later models can use the newly-created Contact’s Id to populate their new records.
  5. Once the save is done, we reload our parent window. This is necessary because this Skuid page will be stuck into an iFrame by Chatter.

Thorny Gotchas, and the undocumented Chatter Publisher API

The basic buildout of this took less than 30 minutes — seriously! However, we spent some time trying to work out how best to “talk” to the Chatter Publisher using the undocumented Chatter Publisher API — an effort which ended in nothing but frustration.

The thorny gotchas to avoid are mostly related to the fact that custom Publisher Actions are implemented using iFrames:

  1. Any  JavaScript you write in your child Visualforce Page will be unable to talk to the parent page UNLESS both your page and the page containing the Chatter component are in the same domain, due to cross-domain script issues — which is impossible unless BOTH pages are Visualforce. Consider these scenarios:
    1. Chatter Feed in “na15.salesforce.com”, Custom Action VF Page in “c.na15.salesforce.com” — BLOCKED.
    2. Chatter Feed in “c.na15.visual.force.com”, Custom Action VF Page in “skuid.na15.visual.force.com” — BLOCKED.
    3. Chatter Feed in “c.na15.visual.force.com”, Custom Action VF Page in “c.na15.visual.force.com” — SUCCEEDS.
    4. Chatter Feed in “skuid.na15.visual.force.com”, Custom Action VF Page in “skuid.na15.visual.force.com” — SUCCEEDS.
  2. We couldn’t find a way, using the undocumented Chatter API, to “refresh” the Chatter Feed to show our newly-created Feed Item, so we had to do a full page refresh to get the Chatter Feed to show this. We tried the following methods, none of which worked:
    1. chatter.getPublisher().resetPublisher()
    2. chatter.getPublisher().submit()
    3. chatter.getFeed().refresh()
    4. chatter.getFeed().showNewUpdates()

We would have LOVED to use one of these methods in our Skuid “saveAll” Snippet, but we’ll have to wait for Chatter to document its API.

Screen shot 2013-05-15 at 1.04.16 PM

We at Skoodat are considering exposing the platform for we use for rapidly building and deploying killer custom UI’s on Force.com — aka Skoodat Skuid. But we’re looking for your feedback: are we crazy, or does this totally excite you? Let us know!

Skoodat Skuid — Wicked cool UI platform for Force.com, or not?

When I hear the words “Reports” and “Managed Packages” in the same sentence, I involuntarily let out a grunt of displeasure. Ask any seasoned ISV, and I guarantee you that the same sour taste fills their mouths. Why? Well, here’s the classic problem: An ISV includes some Reports in their managed package. Now, a common trick for making Reports “dynamic” is to leave one of the Filter Criteria blank and then have its value passed in through query string parameters using the “pv<n>” syntax, where n is the parameter you’d like to populate. For example, in this report of Enrollments at a given School, parameter 2 (0-indexed) is left BLANK:

Then, if we load up this page with query string parameter “pv2” set to the name of a School, like so:

the value that we passed in will be dynamically inserted into the 2nd Filter Criterion, and we’ll have a report on Enrollments at Lincoln High School:

This is awesome, right? Quick, let’s throw a Custom Link on the Account Detail Page called “Enrollments” that links to this report, but passing in the Id of the Report! Yeah, yeah, yeah! I love this!

Hold your horses, partner.

This is where the ISV’s hang their heads in sadness… sorry son, it just ain’t that easy.

What’s the matter, grandpa? Come on, this is child’s play!

Not quite.

Notice where we said that we’d be passing in the ID of the Report. Hard-coded. For an ISV, ID’s are the ultimate  taboo. Why? Well, sure, you can package up the Report and Custom Link. But as soon as you install the package into a customer’s org, the Id of the Report has CHANGED — and the link is BROKEN. It’s a situation that the typical one-org Admin will never have to face, but, well, welcome to the world of the ISV.

Isn’t there one of those handy Global Variables which lets you grab the Name or DeveloperName of a Report?

Nope, sorry partner.

So, what DO you do?

Well, you write a ‘ViewReport’ Visualforce page that takes in the API name of a Report — which does NOT change across all the orgs that a package is installed into — and uses this API name to find the ID of the Report and send you to it. What does this look like?

The Visualforce is simple — one line, to be exact:


<apex:page controller="ViewReportController" action="{!redirect}"/>

The Apex Controller is a little more interesting. Here’s the meat, with test code included that achieves 100% coverage (so you can start using it right away!!!):


public class ViewReportController {

    // Controller for ViewReport.page,
    // which redirects the user to a Salesforce Report
    // whose name is passed in as a Query String parameter

    // We expect to be handed 1-2 parameters:
    // r: the DEVELOPER name of the Report you want to view
    // ns: a salesforce namespace prefix (optional)
    public PageReference redirect() {
        // Get all page parameters
        Map<String,String> params = ApexPages.currentPage().getParameters();

        String ns = params.get('ns'); // NamespacePrefix
        String dn = params.get('dn'); // DeveloperName

        List<Report> reports;

        // If a Namespace is provided,
        // then find the report with the specified DeveloperName
        // in the provided Namespace
        // (otherwise, we might find a report in the wrong namespace)
        if (ns != null) {
            reports = [select Id from Report
                  where NamespacePrefix = :ns
                  and DeveloperName = :dn limit 1];
        } else {
            reports = [select Id from Report where DeveloperName = :dn limit 1];
        }

        PageReference pgRef;

        // If we found a Report, go view it
        if (reports != null && !reports.isEmpty()) {
            pgRef = new PageReference('/' + reports[0].Id);
            // Add back in all of the parameters we were passed in,
            // MINUS the ones we already used: ns, dn
            params.remove('ns');
            params.remove('dn');
            pgRef.getParameters().putAll(params);
        } else {
            // We couldn't find the Report,
            // so send the User to the Reports tab
            pgRef = new PageReference('/'
                + Report.SObjectType.getDescribe().getKeyPrefix()
                + '/o'
            );
        }

        // Navigate to the page we've decided on
        pgRef.setRedirect(true);
        return pgRef;

    }

    ////////////////////
    // UNIT TESTS
    ////////////////////

    // We MUST be able to see real Reports for this to work,
    // because we can't insert test Reports.
    // Therefore, in Spring 12, we must use the SeeAllData annotation
    @isTest(SeeAllData=true)
    private static void Test_Controller() {
        // For this example, we assume that there is
        // at least one Report in our org WITH a namespace

        // Get a report to work with
        List<Report> reports = [
            select Id, DeveloperName, NamespacePrefix
            from Report
            where NamespacePrefix != null
            limit 1
        ];

        // Assuming that we have reports...
        if (!reports.isEmpty()) {
            // Get the first one in our list
            Report r = reports[0];

            //
            // CASE 1: Passing in both namespace, developername,
            // and a parameter value
            //

            // Load up our Visualforce Page
            PageReference p = System.Page.ViewReport;
            p.getParameters().put('ns',r.NamespacePrefix);
            p.getParameters().put('dn',r.DeveloperName);
            p.getParameters().put('pv0','llamas');
            p.getParameters().put('pv2','alpacas');
            Test.setCurrentPage(p);

            // Load up our Controller
            ViewReportController ctl = new ViewReportController();

            // Manually call the redirect() action,
            // and store the page that we are returned
            PageReference ret = ctl.redirect();

            // We should be sent to the View page for our Report
            System.assert(ret.getURL().contains('/'+r.Id));
            // Also, make sure that our Filter Criterion values
            // got passed along
            System.assert(ret.getURL().contains('pv0=llamas'));
            System.assert(ret.getURL().contains('pv2=alpacas'));

            //
            // CASE 2: Passing in both just developername
            //

            // Load up our Visualforce Page
            p = System.Page.ViewReport;
            p.getParameters().put('dn',r.DeveloperName);
            Test.setCurrentPage(p);

            // Load up our Controller
            ctl = new ViewReportController();

            // Manually call the redirect() action,
            // and store the page that we are returned
            ret = ctl.redirect();

            // We should be sent to the View page for our Report
            System.assert(ret.getURL().contains('/'+r.Id));

            //
            // CASE 3: Passing in a nonexistent Report name
            //

            // Load up our Visualforce Page
            p = System.Page.ViewReport;
            p.getParameters().put('dn','BlahBLahBlahBlahBlahBlah');
            Test.setCurrentPage(p);

            // Load up our Controller
            ctl = new ViewReportController();

            // Manually call the redirect() action,
            // and store the page that we are returned
            ret = ctl.redirect();

            // We should be sent to the Reports tab
            System.assert(ret.getURL().contains(
                '/'+Report.SObjectType.getDescribe().getKeyPrefix()+'/o'
            ));

        }

    }

}

And here’s an example of using this code in a Custom Link:

Voila! A ISV-caliber way to link to any Report in any Managed Package, and it won’t break in an org where the package is installed!

The basic flow of the Apex is pretty simple: the redirect method gets called immediately upon page load, and it returns a page reference to redirect the user to. So all that Apex needs to do for us is find the Report with the provided API name / DeveloperName (and optionally in a specific namespace), and send us to /<Id>, where <Id> is the Id of that Report. Pretty straightforward. Just a few interesting points:

  1. We ‘tack-on’ to the resultant page reference any OTHER page parameters that the user passed along, so we can pass in any number of dynamic Filter Criteria parameters using the pv<n> syntax.
  2. You may be wondering — wait, you can QUERY on the Report object? Yep! Reports are technically SObjects, so you can query for them, but they fall under the mysterious category called “Setup” objects which, among other peculiar quirks (Google “MIXED_DML_OPERATION” for one of the more annoying ones), only selectively obey CRUD and only expose some of their fields to SOQL. Fortunately, for our purposes, the Id, Name, DeveloperName, and NamespacePrefix fields are all included in this short list. Actually, fire up SOQLXplorer — you might be inspired by some of the other fields that are exposed.
  3. Namespacing of Reports — Reports included in Managed Packages don’t have to have a globally unique name — they only have to be unique within their Namespace. Therefore, when querying for Reports, it’s best to query for a report within a particular namespace.
  4. If the Report is not found — in our example, we send the User to the Reports tab. You might want to do something different.