Lightning Flow and Test Coverage

If you read on the same sentence Flow and Test, what do you think is the article about? My first thought was “how to test a flow in order to avoid issues.” Something like this one.

However, my entry would like to focus on Test Coverage as it is a key point that could be a pain if you need to deploy Flows in active mode. The reason, they also require to have a 75% of coverage at least, in order to get a success deploy.

Flow Test Coverage? I thought this is a #clicksnotcode tool …

Yes and it may not make sense if you are working with Screen Flow. In fact, we do not need to worry about test coverage on these kind of flows. However, you need to keep in mind those that work without screens such as autolaunched flows. You can have more information here.

And how can we do it? Like the documentation says, via a Trigger.

Let’s try to explain it with an example.

If you have read some others entries, you will already know my Flow examples are based on a Hotel Reservation App. So below example is related to Reservations. The scope of the autolaunched flow is to send an email to the guest once he pays the reservation.

And in order to make it work, I need to call it from my Trigger.

trigger ReservationTest on Reservation__c (before insert, before update)
{
    for(Reservation__c r : Trigger.new)
    {
        if(r.isPaid__c)
        {
            System.debug('Run the email flow');
            
            Map<String, Object> params = new Map<String, Object>();
            params.put('guestEmail', r.guestEmail__c);
                
            Flow.Interview.EmailFlow flowToSendEmail = new Flow.Interview.EmailFlow(params);
            flowToSendEmail.start();
        }
    }
}

Now, that we have the new flow, we need to check Flow Coverage status. But the first execution is to identify all Active elements we have in the org in order to be able to calculate the coverage.

Running this query:

Select MasterLabel,
	Definition.DeveloperName,
	VersionNumber,
	Processtype
 FROM Flow 
 WHERE Status = 'Active' 
    AND (ProcessType = 'AutolaunchedFlow' 
       OR ProcessType = 'Workflow' 
       OR ProcessType = 'CustomEvent' 
       OR ProcessType = 'InvocableProcess')

We get below result, and we can identify 2 Autolaunched flow. One of them is the EmailFlow, the one we are using on this example.

Run on Developer Console with Use Tooling API checked

On the other side, Salesforce provides an object in order to check coverage, FlowTestCoverage and we will use it in order to check EmailFlow one. However it only returns a record if there is already some coverage of the elements, so the easiest way to start checking it is modify previous query adding as a filter where the Id not in on the new FlowTestCoverage object.

Select MasterLabel,
		Definition.DeveloperName,
		VersionNumber,
		Processtype
 FROM Flow 
 WHERE Status = 'Active' 
    AND (ProcessType = 'AutolaunchedFlow' 
       OR ProcessType = 'Workflow' 
       OR ProcessType = 'CustomEvent' 
       OR ProcessType = 'InvocableProcess')
    AND Id NOT IN (SELECT FlowVersionId FROM FlowTestCoverage)

The query result is the same as before so we can start writing our first test in order to cover EmailFlow one.

@isTest
public class EmailFlowApexTest
{
	static testMethod void validateEmailFlow()
    {
        Reservation__c r = new Reservation__c();
        r.isPaid__c = true;
        r.guestEmail__c = 'a@ff.com';
        insert r;
        
	    List<Reservation__c> rList = [Select Id, isPaid__c, guestEmail__c from Reservation__c where Id = :r.Id];
        System.assert(true, rList.get(0).isPaid__c);
    }
}

Now, this test covers the trigger that starts EmailFlow, so if we run previous query again, now, EmailFlow will not appear as part of the result, and running a query against FlowTestCoverage, we can start getting some information about our job.

SELECT Id, 
 	ApexTestClassId, 
 	TestMethodName, 
 	FlowVersionId, 
 	NumElementsCovered, 
 	NumElementsNotCovered 
 FROM FlowTestCoverage

Where we can see how match the FlowVersionId with ours, and also the TestMethodName we defined above, validateEmailFlow.

Now I wonder: What about if our test has more than one step? What does happen with above result? So let’s go for it. After the email is sent, we will have a decision element that will check the total amount of the reservation. After that, do an assignment to a certain object. We could continue adding more complex to the flow but let’s keep it simple so far.

The first step is to modify the test in order to have an amount higher or less than the decision filter.

@isTest
public class EmailFlowApexTest
{
    static testMethod void validateEmailFlow()
    {
        Reservation__c r = new Reservation__c ();
        r.isPaid__c = true;
        r.guestEmail__c = 'agarcia@financialforce.com';
        r.TotalAmount__c = 100;
        insert r;
        
	    List<Reservation__c> rList = [Select Id, isPaid__c, guestEmail__c from Reservation__c where Id = :r.Id];
        System.assert(true, rList.get(0).isPaid__c);
    }
}

With this, if we run below query,

SELECT Id, 
     ApexTestClassId, 
     TestMethodName, 
     FlowVersionId, 
     NumElementsCovered, 
     NumElementsNotCovered 
 FROM FlowTestCoverage

We get a similar result as the previous one. The only difference is that NumElementsCovered is 3 while NumElementsNotCovered is 1 . The reason is because now, our flow has 4 elements, the core one that sends the email, the decision and the 2 assignments. But our test only covers one of the assignments.

So next step is to add a second method that covers the second decision path.

@isTest
public class EmailFlowApexTest
{
    static testMethod void validateEmailFlow()
    {
        Reservation__c r = new Reservation__c ();
        r.isPaid__c = true;
        r.guestEmail__c = 'agarcia@financialforce.com';
        r.TotalAmount__c = 100;
        insert r;
        
	List<Reservation__c> rList = [Select Id, isPaid__c, guestEmail__c from Reservation__c where Id = :r.Id];
        System.assert(true, rList.get(0).isPaid__c);
    }
    
    static testMethod void validateEmailFlowWithAmountLessThan50()
    {
        Reservation__c r = new Reservation__c();
        r.isPaid__c = true;
        r.guestEmail__c = 'agarcia@financialforce.com';
        r.TotalAmount__c = 25;
        insert r;
        
	List<Reservation__c> rList = [Select Id, isPaid__c, guestEmail__c from Reservation__c where Id = :r.Id];
        System.assert(true, rList.get(0).isPaid__c);
    }
}

And this time, the query execution returns below result where we can see the second testMethod.

Also we can check both have same values on NumElementsCovered and NumElementsNotCovered because each one cover a single path.

Note: Whenever I made a modification on my trigger, the query took a while (even a day) to get the results. It’s like Salesforce needed some time to process new test coverages.

Finally I would like to talk about how we know we achieve the 75% required in order to be able to deploy my flow. Unfortunately Salesforce does not provide the information yet but explain a way to calculate it manually.

For example, if you have 10 active autolaunched flows and processes, and 8 of them have test coverage, your org’s flow test coverage is 80%.

Salesforce help: https://sforce.co/2PqALZw

 

Having that, if I only have a single flow in my org with some test coverage, per above info, my flow coverage is 100%. However I would not be surprised if they look into the test flow and check the number of elements. So if I have 4 elements and 2 are covered, my coverage is 50% and if I cover 3, I achieve the 75% required. Keep this in mind just in case.

Update 20190726

While I was writing this post, I found out some weird behaviours with tests. If I removed a test method that covered a flow and run the query again, results were same as if I had the test in place. Because of that, I decided to raise a case with Salesforce in order to know more, and I would really like to share the final answer with you.

When we first run a test class it will add a record for each method to the FlowTestCoverage object. If we modify the class to add a new method and run it again, then, it will add a new record for the new method. But if we remove a method it will not delete the record. The record will only be removed if a change is made to the associated flow version.

If we delete the test class completely it will remove all FlowTestCoverage records for the related class since it no longer exists.

If you would like the FlowTestCoverage object to reflect changes made to test classes then, the best way to do this would be to create a new version of the flow and run the test classes against the new version.

Hope it helps others.

New Lightning Component for Maps

New Lightning Component for Maps

20180126 UPDATE!! I got some new and very useful information about maps. Please, go to Latest Update section if you have already read this entry

A couple of years ago, I wrote a post about how to visualise maps and locations on Visualforce pages, and without having that in mind, Winter ’19 helped me to deliver a second part of that one. Now, Salesforce has released a new Map Lightning Component, so I will give it a try before moving to Spring ’19 version.

You will also find a similar example on Winter ’19 maintenance exam challenge. My code is much simpler than the maintenance one, so maybe it is a good start point before making it more complex.

In a similar way as the old post, I need an object with the information to show. In my case, Doctor object with a record about my Dentist, including a Geolocation field, the Location__c one.

Having that, I need to write some code in order to deliver a Lightning Component that call the standard Map component.

I will start with the Controller in Apex in order to retrieve the information from the database. Remember that Geolocation fields is a compound field with 2 pieces, Latitude__s and Longitude__s.

public with sharing class DoctorMapController
{
   @AuraEnabled
   public static List<Doctor__c> getDoctors()
   {
      List<Doctor__c> doctors = [Select Id,
                                   Name, 
                                   Address__c,
	                           Email__c, 
                                   Phone__c,
	                           SurgeryName__c, 
                                   Location__Latitude__s,
                                   Location__Longitude__s
                                 FROM Doctor__c];
      return doctors;
   }
}

We will continue with the component.

First of all, we implement usual interfaces in order to be able to visualise the component on different areas/views of our environment.

Secondly, I will add the tag <lightning:map> and its properties in order to show map information. The most important is mapMarkers. It is an array of markers that will help us to set the usual “pointer globe” icon that google maps provides. After that, we can add others like markerTitle or zoomLevel but they are not required, they just add more value to the component and the visualisation itself.

So having to provide information to mapMarkers and others, we need attributes in order to retrieve the data from our component Controller. That’s why we have 2 <aura:attribute> They are like variables on an apex class. Define the type and name of the variables that you will use among the component to get and set the data you need.

Finally <aura:handler> that will execute the action that will initialise the component via the javascript Controller. It is like the start point.

<aura:component implements="force:appHostable,
                            flexipage:availableForAllPageTypes,
                            flexipage:availableForRecordHome" 
                controller="DoctorMapController" 
                access="global" >
   <aura:attribute name="mapMarkers" type="Object" access="PRIVATE" />
   <aura:attribute name="markersTitle" type="String" access="PRIVATE" />
   <aura:handler name="init" value="{!this}" action="{!c.init}"/>
   <aura:if isTrue="{!!empty(v.mapMarkers)}" >
      <lightning:map 
		     mapMarkers="{!v.mapMarkers}" 
		     markersTitle="{!v.markersTitle}"
                     zoomLevel="5"
      />
   </aura:if>
</aura:component>

And the init, points to here, the javascript Controller. It depends on the way you develop. Actually you can add all method logics to here, but it is a good practice to have it on the Helper, and on the Controller, just a call to there.

({
   init : function(component, event, helper) {
	helper.initHelper(component, event, helper);
   }
})

Finally a Helper where we will find main methods. If you are familizarize with Javascript, it will not be very difficult to understand.

Basically the variable action, access our Apex Controller getDoctors method in order to retrieve all Doctor__c records.

With this information, we can iterate over there and show all doctors on our map. But my case is much simpler, I get the first one, and populate another variable with its information. Finally I will set the values to the component attributes, markTitle and mapMarkers.

But let me focus for a second on the variable that is assigned to mapMarkers attribute. The variable markers is an array because, as I mentioned at the beginning, mapMarkers parameter is an array with locations. But what do I need to specify there? The main one is Location that could be populated with the Geolocation field data or with a direction itself. After that we only have some more descriptive data like the title or the description. They add value to the map but the required one is Location.

({
   initHelper : function(component, event, helper) {
        helper.defineMarkers(component, event, helper);
   },
   defineMarkers : function(component, event, helper) {
	let action = component.get("c.getDoctors");
	action.setCallback(this, function(response) {
	   const data = response.getReturnValue();
	   const dataSize = data.length;
	   let markers = [];
        
           //I will only show my single record
	   const dentist = data[0]; 
	   markers.push({
                'location': {
                    'Latitude':dentist.Location__Latitude__s,
                    'Longitude':dentist.Location__Longitude__s
                },
                'title' : dentist.SurgeryName__c,
                'description' : 
                      dentist.SurgeryName__c + 
                      ' dentist Location at ' + 
                      dentist.Address__c
	   });
	   component.set('v.markersTitle', 
                         'Out and About Communications Dentist 
                         Locations');
	   component.set('v.mapMarkers', markers);
	});
	$A.enqueueAction(action);
   }
})

And here you can find the result, a map with the “Moyua Square” with a pointer to my dentist place.

Reading this entry you can realise that using <lightning:map> is very simple and that actually, I focused on explain how to build the component itself, but if you would like to know more or create more complex pieces of code, take a look at the documentation and do some searches on internet. I’m sure people share their experiences with the new <lightning:map>.

Latest Update:

After reading this entry, my friend Alba Rivas told me she was doing some researches on maps too, and found out some useful information that shared with me:

If an address is specified for a marker, instead of its latitude and longitude, the component makes API requests to the google geolocation, and per Google documentation, it seems it has a cost. Also I would like to know if there is an event that I can listen when a marker is selected.

The API key is from Salesforce, and there are no current limitations on its use. We are going to add a click handler for selecting a marker in an upcoming release

Alba Rivas question and Salesforce response

From Attachment to File via Flow?

Some days ago I got a message asking me if it was possible to move from Attachments to Files in Salesforce using the #clicksnotcode tool VisualFlow and after some researches I would say no, this is not possible without code. But if you find the way, please, please, please, leave me a message so that I can update this post.

So, what is this post about? My researches and how I got that the answer is no.

First of all, the use case. As you know we cannot create Attachments in Lightning Experience, only Files are available in LEX, so anything that we want to keep should be also migrated.

But how?

  • Manually? That could be a really expensive option, and tired, to be honest.
  • Via code? Yes!! I like this option. At the end, a File is a Salesforce Object, ContentVersion , and as you can find on this blog, with a really simple piece of code you can create a new File record. Below piece adds the query to an attachment. After that, find the image with the new file and also the result of a query done in  Workbench.
List<Attachment> attList = [SELECT 
                               Body,BodyLength,Name,ParentId 
                            FROM Attachment limit 1];
Attachment att = attList.get(0);

String fileContent = att.body.toString();
ContentVersion conVer = new ContentVersion();
conVer.PathOnClient = 'MigratedFile.txt';
conVer.Title = 'MigratedFile.txt';
conVer.Description = att.Name + ' migrated';
conVer.VersionData = EncodingUtil.base64Decode(fileContent);
conVer.FirstPublishLocationId = '001B000000hYespIAC'; 
insert conVer;

Captura de pantalla 2018-06-28 a las 17.15.33

Captura de pantalla 2018-06-28 a las 17.20.22

And what about VisualFlow?

I created something really simple, 4 steps flow. But it has a welcome and a thanks screen, so actually the job is done in 2 steps.

Captura de pantalla 2018-06-28 a las 17.30.46

First one is Record Lookup where I retrieve the single attachment that I have in my organization. Please, keep in mind that if you have a list of Attachments, and use a Record Lookup, it will return a single record, but if your idea is to retrieve all attachments, you need to use a Fast Record Lookup step.

Captura de pantalla 2018-06-28 a las 17.31.10

This step is going to store data into the variable that I created, and all of them are similar, Text variables.

Captura de pantalla 2018-06-28 a las 17.31.48

The second step is to create the ContentVersion record, assigning the variable data that I got on the previous step.

Captura de pantalla 2018-06-28 a las 17.32.00

And that is all. Yes, so simple.

However, when I run it I got an error and this email where I can see that there is an issue on VersionData field.

Captura de pantalla 2018-06-28 a las 17.42.18

The reason is simple. Attachment Body returns a Blob that I need to convert into String in order to be able to encode and pass to the Content file.

....
String fileContent = att.body.toString();
....
conVer.VersionData = EncodingUtil.base64Decode(fileContent);
....

Is there a way to fix it? No as far as I could find. My first idea was to check what the variable was returning, adding a middle step to show the body value

Captura de pantalla 2018-06-28 a las 17.55.06

And as I expected that is not my body text

Captura de pantalla 2018-06-28 a las 17.54.46

So the next thought was to try to create a formula in flow or make any transform, something that could help me, but I couldn’t find any.

Captura de pantalla 2018-06-28 a las 17.51.16

So I think that the only way to do it is via some Apex code. We can create a class that has InvocableMethod annotation and do the transform there and call the class as it was an step, but, to be honest, why do I want to do it when I can run the whole transform from an apex class as I explained before?

At the end, I would advice to create your own ApexClass that execute the process to do the migration if you do not want to use the tool that Salesforce provides.

Good luck!!

BigObjects and Triggers

Some months ago I wrote an entry about BigObjects and since that moment I go lot of comments with questions and some useful information that I missed in my post and I would like to include here because sometimes, we do not read comments.

Not all SOQL statements are valid. For now, the !=, LIKE, NOT IN, EXCLUDES, and INCLUDES operators are not valid in any query.

There are already some Standard Big Objects like FieldHistoryArchive, so do not try to create one directly, and check before if it already exist.

Unfortunately SOSL is not supported by BigObjects.

But maybe the most important one was the fact that people was not able to create BigObject records from Triggers. It was a big surprise for me because documentation said that you could not create triggers related to a Big Object, but nothing about not be able to create Big Object records from triggers. So today I decided to give it a try.

If you have taken a look at my blog, the use case was to archive Code Review records so that we could end up with Code Review History records. Although it doesn’t make too much sense, create History records in a trigger, I decided to make it simple and continue with these objects.

So the first idea was to create a Trigger related to CodeReview__c:

trigger CodeReviewTrigger on CodeReview__c (after insert)
{
   List<CodeReviewHistoric__b> crhToInsert = new List<CodeReviewHistoric__b>(); 
   for (CodeReview__c cr: Trigger.new)    
   { 
      CodeReviewHistoric__b crh = new CodeReviewHistoric__b();        
      crh.CanBeMerged__c = cr.CanBeMerged__c ? 'true' : 'false';        
      crh.CodeReviewDate__c = cr.CodeReviewDate__c;         
      crh.Comments__c = cr.Comments__c;        
      crh.Employee__c = cr.Employee__c;        
      crh.Score__c = cr.Score__c;        
      crh.Story__c = cr.Story__c;        
      crhToInsert.add(crh);    
   }
   Database.insertImmediate(crhToInsert);
}

However, when I tried to create a Code Review record via the UI, I got below error

Captura de pantalla 2018-02-18 a las 15.16.26

and if I checked log, I also got this one.

Captura de pantalla 2018-02-18 a las 15.16.54

So came back to BigObjects product manager suggestion:

However a workaround could be to put the insertImmediate() call into a queueable method

But why queuable method? I think the key point is that we need a different context, a new transaction, so it doesn’t matter the way to get it, and I decided to keep things simple using a @future method in order to perform the Database.insertInmediate action. Then, this method will be called from the trigger.

But this annotation has several restrictions, like argument types. It doesn’t support a list of SObjects, but it allows a List of Ids.

So Trigger will look like this:

trigger CodeReviewTrigger on CodeReview__c (after insert)
{    
   List<Id> codeReviewIds = new List<Id>(); 
   for (CodeReview__c cr: Trigger.new)    
   {        
      codeReviewIds.add(cr.Id);    
   }
   AsyncProcess.createBigObjectRecords(codeReviewIds);
}

And @future one like this:

public with sharing class AsyncProcess
{
   @future
   public static void createBigObjectRecords(List<Id> crIds)
   {
      List<CodeReviewHistory__b> crhToInsert = new List<CodeReviewHistory__b>();
      for (CodeReview__c cr: [Select Id, 
                                     CanBeMerged__c, 
                                     CodeReviewDate__c, 
                                     Comments__c, 
                                     Employee__c, 
                                     Score__c, 
                                     Story__c 
                               From CodeReview__c 
                               Where Id In :crIds])    
      {
         CodeReviewHistory__b crh = new CodeReviewHistory__b();
         crh.CanBeMerged__c = cr.CanBeMerged__c ? 'true' : 'false';            
         crh.CodeReviewDate__c = cr.CodeReviewDate__c;             
         crh.Comments__c = cr.Comments__c;            
         crh.Employee__c = cr.Employee__c;            
         crh.Score__c = cr.Score__c;            
         crh.Story__c = cr.Story__c;            
         crhToInsert.add(crh);    
      }     
      Database.insertImmediate(crhToInsert);    
   }
}

And now it works sucessfuly

Captura de pantalla 2018-02-18 a las 16.37.46

Captura de pantalla 2018-02-18 a las 16.39.40.png

Any other options? Batch Apex, Queueable, even, you can use Async SOQL via a http call.

I’m sure that I still miss things about BigObjects but another amazing resource to check is the Salesforce MVP Alba A. Rivas blogs about this technology.

 

Process billions of records with Async SOQL

Christmas time is closer but I would like to deliver this last post before end of the year.

Some months ago, I wrote an entry talking about BigObjects, a feature that was General Available by Winter ’18 and via a use case, I tried to explain it. Now, I would like to follow that post with a new way to create records, Async SOQL.

Async SOQL is half GA by Winter ’18 and half still in Pilot 

The use case talked about moving Code Review custom object records, into Code Review History big objects records, and release storage of custom objects one. We want to Archive records, and Async SOQL will help us to deal with big amounts of data.

What is Async SOQL?

Basically Async SOQL allows you to run SOQL in the background so that, you will get a response after a period of time, but at the same time, helps you to deal with millions or even billions of records without hitting time outs or governor limits.

How can I execute Async SOQL?

Async SOQL is implemented as REST full API and in order to execute this SOQL in the background we need to run a post request:

Captura de pantalla 2017-11-29 a las 21.51.22

And provide a body in JSON format in order to make the call. Find bellow a really easy example that helps you to create Vendor__c custom object records based on Accounts.

Captura de pantalla 2017-12-19 a las 9.40.12What can we highlight?

query: allows you to define a SOQL. On our case, the object I’m going to read, Account and the field I want to recover, Name.

operation: help us to define what we want to do, insert or upsert (please check big objects entry to understand Primary Key and upsert, as it works in the same way)

Basically that is the main difference between simple SOQL and Async SOQL. With this new feature we can read and create or update records in one go.

targetobject: object we want to use to create new records. In our case, Vendor__c.

Then, we need to specify from where we want to get record field values and where we want to store this info. For that we have 2 keys in this JSON code.

targetFieldMapping: allows you to define source field and target fields

and

targetValueMap: allows you to map a target field with a literal value, for instance, we want all records have Spain as Country__c field value.

How is the response?

When we do the post call, we get a similar response:

Captura de pantalla 2017-11-29 a las 22.01.12

It is also in JSON format and basically it provides similar information that you passed in the post call. Just highlight 3 keys:

jobId: remember this is an asynchronous call so there is an Id related to the background job. You can also use it in the post call via the global variable $Job_Id as part of key value on targetValueMap.

status: helps you to know where is the job. Moving from New to Running, Complete, Failed etc.

message: before executing the call, the code is analyzed. If it realizes about a possible issue, it doesn’t start the call and provides an error message in that field. For instance, below message. My VendorName__c field size was too short to store all Account Names.

Captura de pantalla 2017-11-29 a las 22.11.03.png

How can I stop the execution?

Async SOQL doesn’t provide a UI in Salesforce like other background jobs like Batch Apex or Queueable. But we can make an http delete call passing the jobId as part of the url.

Captura de pantalla 2017-11-29 a las 22.22.02

How can I check the progress?

Similar as before, as we do not have UI, we need to look for another way in order to get this information. For that, we have 2 options:

1º – Make a get post call: Similar to the cancel action, if we pass the JobId as part of the url, and do a get call, the response shows you information abut the background execution that is running in the system.

Captura de pantalla 2017-11-29 a las 22.32.33

2º – Make a SOQL against BatckgroundOperation object: This is a new object where we can see extra information about the job execution.

Captura de pantalla 2017-11-29 a las 22.32.42

But this is not the only new object that we can get in the system. We also have BackgroundOperationResult that will help you to identify any issue during the execution if the result is not the expected one. For instance. Bellow image shows an issue related to a field that is required but the source value is empty. In that case, a new error is logged on this table, but the execution doesn’t stop, it continues till the end.

Just keep in mind that this information would be removed after 7 days.

Captura de pantalla 2017-11-29 a las 22.32.55

And what about Archiving?

Yes, you are right. We promised to show you how to archive Code Review records but tried to explain the whole functionality with a really simple use case. Now it’s time to move to Code Review History use case.

As I mentioned before, we would like to create Code Review History records reading Code Review records. Similar as before we will do a post call but the body would be like this one:

Captura de pantalla 2017-11-29 a las 22.51.40

On BigObjects post, the object was called Test1CodeReviewHistory__b instead of CodeReviewHistory__b

And … that’s all. Simple, isn’t it?

Can I integrate this functionality into Apex?

Yes, of course, at the end we are just making http calls, so we only need to set the proper url, add it on my remote settings and create a string in JSON format for the body.

Captura de pantalla 2017-11-29 a las 22.42.57

Summary

Finally I would like to sum up some key concepts we have talked about.

  1. Async SOQL allows you to run SOQL in the background
  2. It takes some times but allows you to process millions or even billions of records
  3. You do not need to worry about governor or time out limitations
  4. It is implemented as REST full API
  5. You can make as many calls as you want per day but just a single one at a time
  6. You can read and create records in one go. Delete is out of scope
  7. This feature is part GA and part still in Pilot:
    1. Read Standard or Custom objects and create Standard, Custom or Big Objects is in Pilot
    2. Read Big Objects and create Standard, Custom or Big Objects is GA

Captura de pantalla 2017-11-29 a las 22.43.44

Platform Encryption

Summer time, but I do not want to miss the opportunity so share something even if this post is sorter than usual.

A year ago I completed Platform Encryption Trailhead  but I have started working with it few months ago.

What is Platform Encryption and Why do we need it?

You can find more information here, but basically it would allow us to protect some data, so that, it would not be visible. However this does not mean that the data is stored in database with a mask “****”. It is going to be saved as usual, but if your user doesn’t have certain rights, it will not be visible, and he would have a mask “****” as view effect .

Ok, let’s explain a bit better. If I need a field encrypted, because it is a password, I will use the existing Text (encrypted) field type. If I need to have my data visible in my organization, but if this is stoles, for instance, it should not be visible, then, I need to use existing field types, but enable Platform Encryption on those that have sensible data.

How to enable Platform Encryption?

First of all, you need to have rights, to manage encrypted data. For that, the first thing to do is to create a Permission Set, under System Mode, select Manage Encryption Keys.  Remember to assign it to your user.

If you need to see also encrypted data, you need to add to the same Permission Set, or a new one, View Encrypted Data right.

Then, you need to go to Setup, under Administration Setup you can find Platform Encryption

Captura de pantalla 2017-08-11 a las 18.36.28

The very first time you also need to generate a Tenant Secret just clicking on the button.

Captura de pantalla 2017-08-14 a las 11.40.38

Captura de pantalla 2017-08-14 a las 11.44.24

 

The last step is to decide what you need to encrypt. Right now Salesforce allows you to encrypt certain fields on certain Standard Objects.

Leads fields are in beta

And you can also encrypt custom fields on Standard and Custom objects as long as their field type are: Email, Phone, Text, Text Area, Long Text Area, URL, Date or Date/Time.

But before encrypt any field you need to be aware that there are certain limitations. Maybe one of the most important one is that you cannot filter, order or group by an encrypted field, so that your code can fail once you enable this feature in your organization.

 

Reviewing the code

Now it is time to review your current code to check if there is something failing now. The easiest example of failure is this SOQL in your code.

Select Id, Name From Account Where Name = :accName;

But now it fails at compilation time. And if you turn Encryption off, it continues failing … so what should you do at this point?

If your next step is to move to dynamic SOQL, it’s a good try. Now it saves, but it would fail at run time.

You need to find another way to get the information. Salesforce suggests to use SOSL, and this is an easy solution, as we only need to move to this piece of code. So let’s go for it and solve this issue.

Account acc = new Account();Account acc = new Account();        
acc.Name = 'PE Blog Test';        
insert acc;         

List<List<Account>> accList = [FIND 'PE Blog Test' 
                               IN ALL FIELDS 
                               RETURNING Account(Id, Name)];        

System.debug('Testing: ' + accList);

Captura de pantalla 2017-08-14 a las 12.08.35

Reviewing Unit Tests

Ok, this section sound similar to the previous one. Yes, you are right, but I wanted to highlight it here, because I was not used to work with SOSL and I found something interesting to share.

If I have below method:

public static testMethod void testAcountsWithSOSL() 
{ 
   List<Account> accList = [Select Id from Account]; 
   System.assertEquals(0, accList.size(), 'Error: There should not be any account.');
   
   Account acc = new Account(Name = 'Test'); 
   insert acc;
   accList = [Select Id from Account]; 
   System.assertEquals(1, accList.size(), 'Error: There should be an account.'); 

   List<List<Account>> accList2 = [FIND 'Test' IN ALL FIELDS RETURNING Account];
   System.assertEquals(1, accList2.get(0).size(), 'Error: There should be an account. And result is = ' + accList2); 
}

And run the test, the result is the below one:

Captura de pantalla 2017-08-14 a las 12.14.31

Exactly, it fails because accList2 is empty although I was expecting to get an element.

Doing some researches, I found this article about unit test. So it seems they provide a Test method that allows you to insert the Id of the record you need to look for and get it back. So that, the solution would be this:

public static testMethod void testAcountsWithSOSLFollowingGuide() 
{ 
   List<Account> accList = [Select Id from Account]; 
   System.assertEquals(0, accList.size(), 'Error: There should not be any account.');

   Account acc = new Account(Name = 'Test'); 
   insert acc;
   accList = [Select Id from Account]; 
   System.assertEquals(1, accList.size(), 'Error: There should be an account.'); 

   Id [] fixedSearchResults= new Id[1]; 
   fixedSearchResults[0] = acc.Id;
   Test.setFixedSearchResults(fixedSearchResults); 
   List<List<SObject>> searchList = [FIND 'Test' IN ALL FIELDS RETURNING Account(Id, Name)]; 
   System.assertEquals(1, searchList.get(0).size(), 'Error: There should be an account. And result is = ' + searchList);    
}

Captura de pantalla 2017-08-14 a las 12.19.46

Ok, so doing in this way, you are saved. However, be aware that your tests have the SeeAllData=false and you do not expect to get data from your org, other wise, it would fail too.

I hope this post help you to be closer to understand what you need to do before enabling Platform Encryption in your org.

Picklist dinámicos en Lightning Components

Mis últimos post técnicos en Español eran bastante básicos. Explicaba cómo crear páginas visualforce, controllers, etc. Sin embargo, desde entonces, Lightning Experience y Lightning Components han cogido el protagonismo en Salesforce.

Así que en esta entrada voy a explicar como crear una lista desplegable y poder determinar su valor por defecto desde App Builder, de forma que cada usuario puede decidir qué valor le conviene.

Caso de Uso:

Como usuario, me gustaría tener un lightning component que me muestre los eventos de Salesforce a partir de una ciudad elegida y decidir el valor por defecto de esa lista desplegable.

Captura de pantalla 2017-08-02 a las 18.28.06

Pasos:

Lista desplegable con valores fijos

Comencemos creando un componente con determinadas ciudades pero sus valores son fijos. Para poder testearlo, lo mostraré en la pestaña Home de mi App Salesforce Events.

Para no alargar el post pondré una breve explicación de lo más importante en el código

Captura de pantalla 2017-08-03 a las 16.58.42

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" 
      implements="flexipage:availableForAllPageTypes" 
      access="global">
    
   <!-- Lista de ciudades sobre la que iterar -->
   <aura:attribute name="cities" type="List" />  
   
   <!-- Valor seleccionado para poder usarlo más adelante.-->
   <!-- Indicamos que el valor por defecto es "Madrid" mediante hardcode -->
   <aura:attribute name="selectedValue" type="String" default="Madrid"/> 
   
   <!-- handler donde llamamos al método que me devuelve los valores de la lista -->
   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />    
   
   <!-- Usando Lightning Design System, creamos la lista desplegable -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="{!v.selectedValue}">    
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration> 
   </lightning:select>
   
</aura:component>

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)    
   {
      //asigno a la variable cities de mi component los nombres de las ciudades
      component.set('v.cities',['London','Madrid','Paris','San Francisco']);
   }
})

Lista desplegable con valores dinámicos

El siguiente paso es recuperar de la base de datos los valores de la ciudades que tenemos. Según la imagen de más arriba, de la lista desplegable desaparecería San Francisco.

Captura de pantalla 2017-08-03 a las 17.10.17

Para ello modificaría el Controlador JavaScript que llamaría a un Controlador en Apex dónde hacemos la búsqueda en base de datos.

SalesforceEventsController.cls

//Esta clase puede ser también global
public with sharing class SalesforceEventsController
{
   //Recuerda añadir la anotación @AuraEnabled 
   //para el que método sea visible en el componente
   @AuraEnabled public static List<String> getCityNames()
   {        
      List<String> cityNames = new List<String>();               
      
     //TODO: Recuerda SOC por lo que una SOQL debería hacerse en otra clase 
     //destinada a ello.
     for(City__c c : [Select Id, Name From City__c])        
      {            
         cityNames.add(c.Name);        
      }                
      return cityNames;    
   }
}

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)
   {        
      var action = component.get("c.getCityNames");
      
      //asigno a la variable cities de mi component 
      //los nombres de las ciudades que he recuperado del controlador apex
      action.setCallback(this, 
                         function(a){
                            component.set("v.cities", a.getReturnValue());
                         });
      $A.enqueueAction(action);    
   }
})

Lista desplegable con valores dinámicos y Tabla con información

El siguiente paso es mostrar los valores de los eventos de Salesforce a partir de una selección en la lista desplegable.

SalesforceEventsController.cls

@AuraEnabled  
public static List<SalesforceEvent__c> getEventsByCity(String cityName)
{
    City__c theCity = [Select Id From City__c Where Name = :cityName Limit 1];        
    Id theCityId = theCity.Id;                
    List<SalesforceEvent__c> sEvents = new List<SalesforceEvent__c>();        
    
    //TODO: Recuerda SOC por lo que una SOQL debería hacerse en otra clase 
    //destinada a ello.
    for(SalesforceEvent__c e : [Select Id,EventName__c,EventDate__c From SalesforceEvent__c Where City__c = :theCityId])        
    {            
        sEvents.add(e);        
    }        
    return sEvents;    
}

En este punto me di cuenta que el refresco iba a ser más complicado de lo que tenía pensado ya que tuve que crear un componente hijo para la parte de los Salesforce Events, y un evento para poder hacer el refresco. Os dejo el enlace que usé como referencia.

SalesforceEventsEvent.evt

<aura:event type="APPLICATION">
   <!-- A partir de una ciudad seleccionada, calculo los eventos -->
   <aura:attribute name="citySelected" type="String" />
</aura:event>

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" implements="flexipage:availableForAllPageTypes" access="global">
   <!-- Attributes set on Design Page-->    
   <aura:attribute access="global" name="headerText" type="String" /> 
   <aura:attribute access="global" name="defaultCity" type="String" />        
   
   <aura:attribute name="cities" type="List" />    
   <aura:attribute name="selectedValue" type="String" default="Madrid"/>    

   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />

   <!-- onchange no funciona sin un handler relacionado con value el tag option -->
   <aura:handler name="change" value="{!v.value}" action="{!c.cityChange}"/>        

   <!-- Usaremos onChange para llamar a la función js que refrescará la lista -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="selectedValue" onchange="{!c.cityChange}"> 
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration> 
   </lightning:select>        

   <h1>Salesforce Events:</h1>    
   <!-- Registro el evento y llamo al nuevo componente hijo para listar los eventos de Salesforce -->
   <aura:registerEvent name="appEvent" type="c:SalesforceEventsEvent"/>    
   <c:SalesforEventList />

</aura:component>

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)    
   { 
      var action = component.get("c.getCityNames"); 
      action.setCallback(this, function(a) 
      { 
         component.set("v.cities", a.getReturnValue());        
      });        
      $A.enqueueAction(action);    
   },        
   cityChange : function(component, event)    
   {        
      //Determino el nuevo valor por defecto que es el elegido
      component.set('v.selectedValue', component.find("mySelect").get("v.value"));        
      component.find("mySelect").get("v.value");
      
      //Paso al atributo citySelected del event el nuevo valor seleccionado
      var appEvent = $A.get("e.c:SalesforceEventsEvent");        
      appEvent.setParams({"citySelected" : component.get("v.selectedValue")});        
      appEvent.fire();    
   }
})

SalesforEventList.cmp

<aura:component controller="SalesforceEventsController" implements="flexipage:availableForAllPageTypes" access="global">

   <aura:handler event="c:SalesforceEventsEvent" action="{!c.loadEvents}"/>
   <aura:attribute name="cityToFilter" type="String" />    
   <aura:attribute name="sevents" type="SalesforceEvent__c[]" />         

   <aura:iteration items="{!v.sevents}" var="theEvent">        
      <li>{!theEvent.EventDate__c} - {!theEvent.EventName__c }</li>    
   </aura:iteration> 
</aura:component>

SalesforEventList.cmp

({ 
   loadEvents : function(component, event)    
   {        
      var citySentFromEvent = event.getParam("citySelected");
      component.set("v.cityToFilter", citySentFromEvent);            

      var action = component.get("c.getEventsByCity");        
      action.setParams({ cityName : component.get("v.cityToFilter") }); 
      action.setCallback(this, function(a) 
      { 
         component.set("v.sevents", a.getReturnValue());        
      });        
      $A.enqueueAction(action);                
   }
})

Determinar un valor por defecto desde App Builder

La idea aquí es dar la opción de poner el valor por defecto cómo parte de la configuración. Para ello usaremos la funcionalidad Dynamic PickList for Lightning Components.

Añadiremos 2 atributos a nuestro componente. Uno sera un texto con información y el segundo nuestra lista desplegable.

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" 
                implements="flexipage:availableForAllPageTypes" 
                access="global">
   
   <!-- Atributos para la Design Page--> 
   <!-- Este primer atributo no es requerido para la lista desplegable -->
   <aura:attribute access="global" name="headerText" type="String" /> 
   <!-- En vez de tener el valor por defecto con hardcode, usamos el atributo defaultCity -->
   <aura:attribute name="selectedValue" type="String" default="{!v.defaultCity}"/>

   <aura:attribute name="cities" type="List" />     
   <aura:attribute name="selectedValue" type="String"/>    
   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />    

   <!-- Utilizo como valor para value el atributo defaultCity -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="selectedValue">
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration>
   </lightning:select>
...
</aura:component>

A continuación debemos crear nuestro design para añadir los nuevos campos a la Design Page.

SalesforceEventsComponent.design

<design:component label="Salesforce Events Cities">
   <design:attribute name="headerText" 
                     label="Header Text" 
                     description="Text that will appear in the header when the component is displayed" 
                     default="City" />    
   <!-- datasource nos ayuda a lincar 
        el atributo con una clase apex de dónde extraeremos 
        la información que necesitamos -->
   <design:attribute name="defaultCity" 
                     label="Default City PickList" 
                     datasource="apex://SalesforceEventsCityValuePickListDefault" />
</design:component>

Por último, debemos crear la clase apex lincada a nuestro atributo.

Aunque en la documentación aparece como global, si la hacemos pública funciona igualmente.

Esta clase extiende VisualEditor.DynamicPickList y aunque tiene cuatro métodos para sobre escribir, sólo son requeridos dos. Uno determina el valor por defecto en la lista desplegable, y el otro nos muestra los valores.

SalesforceEventsCityValuePickListDefault.cls

public class SalesforceEventsCityValuePickListDefault extends VisualEditor.DynamicPickList
{ 
   public override VisualEditor.DataRow getDefaultValue()    
   {        
      VisualEditor.DataRow defaultValue = new VisualEditor.DataRow('defaultLabel', 'defaultValue');        
      return defaultValue; 
   }        

   public override VisualEditor.DynamicPickListRows getValues()    
   { 
      VisualEditor.DataRow defaultRow = new VisualEditor.DataRow('defaultLabel', 'defaultValue'); 
      VisualEditor.DynamicPickListRows myRows = new VisualEditor.DynamicPickListRows(); 
      myRows.addRow(defaultRow);              

      List<String> cities = SalesforceEventsController.getCityNames();
      for(Integer i=0; i<cities.size(); i++)
      {
         VisualEditor.DataRow newRow = new VisualEditor.DataRow(cities.get(i), cities.get(i));
         myRows.addRow(newRow);
      }        
      return myRows; 
   }
}

Como muestro en la imagen de abajo, si abrimos el App Builder y seleccionamos nuestro Lightning Component, veremos los dos atributos que definimos en SalesforceEventsComponent.design siendo uno de ellos la lista desplegable con los valores que hemos recuperado de base de datos.

Captura de pantalla 2017-08-03 a las 20.03.46.png

Un último apunte antes de terminar el post. No estoy cubriendo error handling en mi código pero échale un vistazo a la ayuda de Salesforce para ello. Por ejemplo este y este.

Como has podido comprobar, es muy sencillo. Si eres nuevo en este tema, los pasos son fáciles de seguir. Si ya eres un experto, y has encontrado algo en el código que se puede mejorar. Dímelo y así otros podrán beneficiarse de tus conocimientos también.

Salesforce BigObjects

What is a BigObject?

Two years ago I started working with Big Objects, a Salesforce pilot that looked very interesting for me. And now that it seems that General Available time is closer, I would like to write about them.

Please, take into account that a Salesforce pilot could be discarded like Data Pipeline that it would not be GA at the end.

Big Objects is a new type of objects that Salesforce provides. Maybe the most important thing I can tell is that they do not count against the storage limit. Well, you could say that External Objects already do.

You can learn more about External Objects here.

Yes, you are right. But this time, data lives in your organisation instead of in an external storage and this means that you can work with them like with Custom / Standard objects, but if you go to Salesforce Storage screen, you would not find them.

Big Objects let you store and manage large amounts of data.

Use Case

But before starting with the explanation, let’s talk about the Use Case.

As a company that creates software, we have stories in order to develop new functionality. Before delivering them, we need to pass a code review, so that, we ensure the best to our end users.

Captura de pantalla 2017-07-07 a las 12.21.47

But how much storage should I use to keep code review records in our Production org? Should I remove them? If so, what about auditing?

BigObjects is your solution, so that, you can move old records to a new Code Review History BigObject record. Let’s focus for now on this new History object.

How to Create a BigObject?

Unfortunately you cannot do it in a declarative way. You need to define them via Metadata API and then do a deployment via cmd or workbench.

Before doing it, take into account:

  1. Indexes are required and in order to include them, you need to be on API 39.0 onwards.
  2. Once you deploy one, you cannot make any modification like amend a field type, change label, etc. However in a declarative way you can do small changes like the label and API name.
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
  <deploymentStatus>Deployed</deploymentStatus>
  <fields>
    <fullName>CanBeMerged__c</fullName>
      <label>Can Be Merged</label>
      <length>80</length>
      <type>Text</type>
    </fields> 
  <fields>
  <fullName>CodeReviewDate__c</fullName>
    <label>Code Review Date</label>
    <type>DateTime</type>
    <required>true</required>
  </fields>
  <fields>
    <fullName>Comments__c</fullName>
    <label>Comments</label>
    <length>255</length>
    <type>Text</type>
  </fields>
  <fields>
    <fullName>Employee__c</fullName>
    <label>Employee</label>
    <referenceTo>Employee__c</referenceTo>
    <relationshipName>Employees</relationshipName>
    <required>true</required>
    <type>Lookup</type>
  </fields>
  <fields>
    <fullName>Score__c</fullName>
    <label>Score</label>
    <precision>1</precision>
    <scale>0</scale>
    <type>Number</type>
  </fields>
  <fields>
    <fullName>Story__c</fullName>
    <label>Story__c</label>
    <referenceTo>Story__c</referenceTo>
    <relationshipName>Stories</relationshipName>
    <required>true</required>
    <type>Lookup</type>
  </fields>
  <indexes>
    <type>PRIMARY</type> 
    <fullName>Test1CodeReviewPK</fullName>
    <fields>
      <name>Story__c</name>
      <sortDirection>DESC</sortDirection> 
    </fields>
    <fields>
      <name>Employee__c</name>
      <sortDirection>DESC</sortDirection>
    </fields>
    <fields>
      <name>CodeReviewDate__c</name>
      <sortDirection>DESC</sortDirection>
    </fields>
  </indexes>
  <label>Test1 Code Review History</label>
  <pluralLabel>Test1 Code Reviews History</pluralLabel>
</CustomObject>

Once you deploy it into your organization, you would find something like this under BigObject entry:

I decided to name is as Test1 Code Review History because once it is created I cannot modify it, and I didn’t want to use Code Review History label for now.

Captura de pantalla 2017-07-14 a las 13.03.54.png

What can we highlight?

  1. The API name ends with __b instead of __c or __x like custom and external objects.
  2. We will not find Standard fields.
  3. We can create as many custom fields as we want but right now it only supports these field types: DateTime, Lookup, Number, Text and Long TextArea. What about the rest? Try to be creative. For instance a Checkbox can be converted into number or text.
  4. You will not find any other section like PageLayout or Buttons for now.
  5. You will not find Triggers section. It is not allowed for now.
  6. You will not be allowed to create a custom tab related to this object. That is related to previous point and the PageLayout. To visualise Big Object records, you need to create a visualforce page or a lightning component.
  7. You can determine its CRUD and FLS via Profile or Permission Sets during the deploy process or in a declarative way. However for now BigObjects can be only created and read.

Captura de pantalla 2017-07-14 a las 13.44.36

How Can I Create Records?

There are different ways to create BigObject record, like using a csv file, use APIs like Bulk API or even Async SOQL, another Pilot I will talk about in a future entry.

In any case, here, I will focus on Apex.

As I mentioned above, you can only Read or Create records, and nowadays, you cannot use simple DML operations, there is a new one: database.insertImmediate(record)

Test1CodeReview__b cr = new Test1CodeReview__b();
cr.CanBeMerged__c = 'False';
cr.CodeReviewDate__c = System.today();
cr.Comments__c = 'I found a SOQL inside of a loop.';
cr.Employee__c = 'a6j24000000fxSL';
cr.Score__c = 0; 
cr.Story__c = 'a6k24000000k9bN';
database.insertImmediate(cr);

But something pretty cool that it was not available at the beginning, it is the fact we can use this method, to also update records. Actually it works like upsert. If I make a call where the record has a primary key that doesn’t exist, it would create a new record as we can see in the below image.

Captura de pantalla 2017-07-14 a las 14.28.04

However if the value already is in the platform, it will make a modification.

Test1CodeReview__b cr = new Test1CodeReview__b();
cr.CanBeMerged__c = 'False';
cr.CodeReviewDate__c = System.today();
cr.Comments__c = 'I found a SOQL inside of a loop.';
cr.Employee__c = 'a6j24000000fxSL';
cr.Score__c = 1; //Change to 1
cr.Story__c = 'a6k24000000k9bN';
database.insertImmediate(cr);

Captura de pantalla 2017-07-14 a las 14.35.49

If I come back to the original use case, I would like to move CodeReview__c records to Test1CodeReviewHistory__b records. In order to do that I only need to retrieve the record I need and create a new BigObject entry.

CodeReview__c cr = [Select Id, CanBeMerged__c, 
                           CodeReviewDate__c, Comments__c, 
                           Employee__c, Score__c, Story__c 
                    From CodeReview__c 
                    Limit 1];

String canBeMerged = cr.CanBeMerged__c == true ? 'True' : 'False';

Test1CodeReview__b crh = new Test1CodeReview__b();
crh.CanBeMerged__c = canBeMerged;
crh.CodeReviewDate__c = cr.CodeReviewDate__c;
crh.Comments__c = cr.Comments__c;
crh.Employee__c = cr.Employee__c;
crh.Score__c = cr.Score__c; 
crh.Story__c = cr.Story__c;
database.insertImmediate(crh);

How Can I Visualise Records?

As we have already mentioned, we cannot create a custom tab related to a BigObject and show all records. However we can query BigObjects, so what about if we create a custom page for that?

First of all we would need a controller. Really simple one. We have a method to retrieve all records and a get and set to show in a list what we retrieve.

public with sharing class CodeReviewController
{
   private static List<Test1CodeReview__b> codeReviewHistoryList; 
   
   public CodeReviewController()
   {
      setCodeReviewHistoryList(calculateCodeReviewHistoryList());
   } 

   public List<Test1CodeReview__b> calculateCodeReviewHistoryList()
   {
      return [SELECT Id, CanBeMerged__c, CodeReviewDate__c, 
                     Comments__c, Employee__c, Score__c, 
                     Story__c                
               FROM Test1CodeReview__b];
   }

   public static List<Test1CodeReview__b> getCodeReviewHistoryList()
   {
      return codeReviewHistoryList;
   }

   public static void setCodeReviewHistoryList(List<Test1CodeReview__b> value)
   {
      codeReviewHistoryList = value;
   }
}

Secondly, we have the visualforce page. Something to highlight is that right now, the standardController attribute doesn’t work, so we can only create a page with a customController. 

<apex:page showHeader="true" sidebar="true" controller="CodeReviewController">
   <apex:form>
      <apex:pageBlock title="Code Review History Records">
         <apex:pageBlockSection title="Code Review History" columns="1" collapsible="false">
	    <apex:pageBlockTable value="{!codeReviewHistoryList}" var="crHistory">
               <apex:column headerValue="Ready To Merge?">
	          <apex:outputField value="{!crHistory.CanBeMerged__c}"/>
               </apex:column>
	       <apex:column headerValue="Score">
	          <apex:outputField value="{!crHistory.Score__c}"/>
	       </apex:column>
               <apex:column headerValue="Comments">
	          <apex:outputField value="{!crHistory.Comments__c}"/>
               </apex:column>
               <apex:column headerValue="Story">
	          <apex:outputField value="{!crHistory.Story__c}"/>
               </apex:column>
               <apex:column headerValue="Reviewer">
                  <apex:outputField value="{!crHistory.Employee__c}"/>
               </apex:column>
               <apex:column headerValue="Code Review Date">
                  <apex:outputField value="{!crHistory.CodeReviewDate__c}"/>
               </apex:column>
            </apex:pageBlockTable>
         </apex:pageBlockSection>
      </apex:pageBlock>
   </apex:form>
</apex:page>

And the result is this one that includes the record created from scratch and the one that is retrieved from CodeReview__c object.

Captura de pantalla 2017-07-17 a las 9.14.52

Summary

I would like to share this table that summarise BigObjects and compare with Custom Objects. But before, I would like to also highlight that this new object type can be included in a package. You would find under CustomObject section:

Captura de pantalla 2017-07-17 a las 9.16.06

Summary Table:

Feature Custom Object Big Object
Creation Manual
Metadata
Metadata
API name myObject__c myObject__b
Track Activities
Track Field History, etc.
Options Available Options No Available
Field Types All Text ; Date/Time ; Lookup
Number ; Long Text Area
Able to edit fields Yes Yes (with restrictions)
Able to delete fields Yes No
Triggers; Field Sets; etc Options Available Options No Available
Reports Yes No
How to Populate records All CSV file
API (Bulk, SOAP)
Apex
Async SOQL
Can I amend a record? Yes Yes (with restrictions)
Can I see data creating a Tab Yes No
For free? Yes No — Talk with Salesforce
Storage It count against storage It doesn’t count against storage

Maps and Visualforce

Since I was checking Force.com documentation in order to pass Platform Developer Transition exam I had the opportunity to learn more about some topics that I don’t usually use on my daily job.

One of those are Maps, and I’m not talking about Collections (Lists, Sets and Maps). I would like to talk about locations on this post.

Till some releases ago, the only way to add a location into your visualforce page, was via JavaScript. Let’s see an example. (Bellow use case is based on this Salesforce Trailhead)

I’m working on a healthcare app, and we would like to show a map on every doctor’s record, so that, the customer would be able to find the location easily.

In order to get it, I will create a visualforce page with the map and include it in the standard layout.

JavaScript Map Version

First thing is to add the listener:

 google.maps.event.addDomListener(window, 'load', initialize);

that call an initialize function

 var map;
 
 function initialize() {
     var mapOptions = {
         center: new google.maps.LatLng(43.2616465, -2.9393368),
         zoom: 15
     };
     map = new google.maps.Map(document.getElementById("map-canvas"), 
                               mapOptions);
     loadDoctor();
 }

Where we define map properties, like latitude, longitude and zoom.

But this function call another, loadDoctor 

 function loadDoctor()
 {
     Visualforce.remoting.Manager.invokeAction(
         '{!$RemoteAction.DoctorsController.findTheDoctor}',
         '{!Id}',
         function(result, event)
         {
             if (event.status) {
                 var name = result.SurgeryName__c;
                 var lat = result.Location__Latitude__s;
                 var lng = result.Location__Longitude__s;
                 addMarker(name, lat, lng);
             } else {
                 alert(event.message);
             }
        },
        {escape: true}
     );
 }

Above function uses a RemoteAction method, so we know that a Controller is included on this page. This communication with the controller just returns information about the Doctor’s records like his place name and the Location. Maybe you are wondering what below line means.

Location__Latitude__s  and  Location__Longitude__s

Location is a Geolocation field, and, in the end, it is a Compound field, so in order to see the information, we need to decouple into its “elements”, Latitude and Longitude.

Finally, on above method, we have a call to another function that will add the marker on the google map

 function addMarker(name, lat, lng)
 {
     var marker = new google.maps.Marker
           ({
               position: new google.maps.LatLng(lat, lng),
               map: map,
               title: name
           });
 }

With this code, the result is this one:

Captura de pantalla 2016-07-30 a las 22.17.58

Apex Map Version

Is there a different way to get the same result? Yes !! Since Spring ’15 we have a new tag available on visualforce pages: apex:map and with this piece of code:

<apex:map width="100%" height="100%" mapType="roadmap" zoomLevel="15"
          center="43.2616465, -2.9393368">
    <apex:mapMarker title="{!Doctor__c.SurgeryName__c}" 
                    position="{!Doctor__c.Location__Latitude__s},
                              {!Doctor__c.Location__Longitude__s}">
    </apex:mapMarker>
</apex:map>

we can get exactly what we had on the javascript code as we can see on below image that compares both maps. Left side uses apex:maps whereas on the right side the map is created with javascripts.

Captura de pantalla 2016-07-30 a las 22.24.42

More about apex:map

Attributes

  • Height and Width could be defined as pixels or percentage
  • Center could be used with Location data, Address and also with JSON format. From my point of view, it would be easier to create a method in the controller with the data
public Map<String,Double> getDoctorsCenter()
{
    Map<String,Double> mapCenter = new Map<String,Double>
                                   {
                                      'latitude' => 43.2616465,
                                      'longitude' => -2.9393368
                                    };
    return mapCenter;
}

and use the method in the visualforce page.

<apex:map width="100%" height="100%" mapType="roadmap" 
          zoomLevel="15"
          center="{!doctorsCenter}">

Above example set geolocation with hardcode but if we have this information on data base, the code could be safer and don’t fail if there is any change on this data.

MapMarker

  • Similar as before, position attribute could be defined based on Address, JSON or using the Geolocation field. On above example, we directly used the compound sub-fields, but it would be also fine to get this information in the controller, create the map and call the method in the page as we show on center attribute.
  • If your idea is to show all doctors around your city, instead of adding an mapMarker per record, remember to use apex:repeat tag
<apex:map width="100%" height="100%" mapType="roadmap" zoomLevel="15"
          center="43.2616465, -2.9393368">
    <apex:repeat value="{!locations}" var="pos">
        <apex:mapMarker position="{!pos}"/>
    </apex:repeat>
</apex:map>
  • Another mapMarker attribute is icon that help us to show a different image than the red bubble that google offer to us. A good way to show it is using a static resource and make a call to it.
<apex:mapMarker title="{!Doctor__c.SurgeryName__c}" 
                position="{!Doctor__c.Location__Latitude__s},
                          {!Doctor__c.Location__Longitude__s}"
                icon="{!URLFOR($Resource.MapMarkers, 'myIcon.png')">
</apex:mapMarker>

MapInfoWindows

  • This is another tag to show even more information. So for instance, below code show the doctor’s name and phone if we click on the marker icon
<apex:map width="100%" height="100%" mapType="roadmap" zoomLevel="15"
          center="43.2616465, -2.9393368">
    <apex:mapMarker title="{!Doctor__c.SurgeryName__c}" 
                    position="{!Doctor__c.Location__Latitude__s},
                              {!Doctor__c.Location__Longitude__s}">
        <apex:mapInfoWindow >
            <apex:outputPanel layout="block" style="font-weight: bold;">
                <apex:outputText >{!Doctor__c.SurgeryName__c}
                </apex:outputText>
            </apex:outputPanel>
            <apex:outputPanel>
                <apex:outputText >{!Doctor__c.Phone__c}
                </apex:outputText>
            </apex:outputPanel>
        </apex:mapInfoWindow>
    </apex:mapMarker>
 </apex:map>

 

Captura de pantalla 2016-08-02 a las 17.58.24.png

JavaScript vs apex:map

  1. Unfortunately, we cannot create a page with map tag in a Developer Edition, however JavaScript is available on Developer editions. But if you want to give it a try, create your own Trial edition.
  2. As JavaScript is executed on the client side, the map is loaded quicker than the one that use apex:map.
  3. On the other side, Salesforce takes care of any apex tag so any change on code versions, or browser, etc. that could make the code fail, Salesforce will deal with it.
  4. JavaScripts needs a method in the controller in order to get Location information dynamically and if we want to show the map inside a Standard layout, we must define the RemoteAction method as global that makes you define the class as global as well. This is not important till you add the code inside a package. As a reminder, global classes are available to everybody that install your package and maybe this is not your desire.
  5. RemoteAction is not able to get parameters from url. So I cannot do something like
Id currentDoctorId = controller.getId()

and then use it on the RemoteAction method. However,  in our case, we can use directly the Id of the record on the visualforce page as a workaround:

Visualforce.remoting.Manager.invokeAction(
         '{!$RemoteAction.DoctorsController.findTheDoctor}',
         '{!Id}',
         function(result, event){....});

Resources

  1. Apex:map Salesforce documentation
  2. Github Repository with code used on above examples

Mi primera Visualforce IV – Controladores II

¡Cuánto tiempo! Pues si, en Enero fue mi último post en Español y 11  meses después vuelvo a escribir una entrada en nuestra lengua materna. Y es que últimamente he estado un pelín ocupada con mi bebe de 7 meses.

Esta será la última entrada con el título Mi primera Visualforce ya que terminaremos de cubrir conocimientos básicos. Pero no significa que no seguiré escribiendo en Español. Seguid atentos porque publicaré cosas.

¡Pero vamos al lío! Todos los objetos tienen asociados un controlador estándar que le proporciona funcionalidad (Échale un vistazo a esta entrada). Además, podemos crear controladores personalizados para extender y dar más potencia a nuestra página.

Además los controladores pueden tener un único elemento como argumento, como expliqué en mi última entrada, ó a una lista de registros.

Controlador asociado a varios registros

 

Controlador Estándar

Como recordarás, para usar el controlador estándar, sólo tienes que usar el atributo standardController del tag apex:page

<apex:page standardController="Account">
</apex:page>

Entonces, ¿cuál es la diferencia? Como supondrás, necesitaré una variable para poder almacenar todas las cuentas, y después iterar sobre ella. Para ello, apex:page ofrece otro atributo recordsetVar cuyo valor será la lista de registros. Una vez que la tengamos, sólo tenemos que mostrar los campos que deseemos de nuestra cuenta. En el código de más abajo, usaremos el tag apex:repeat aunque podríamos haber usado también apex:datatable como en posts anteriores.

<apex:page standardController="Account" recordsetVar="records">
   <apex:form >
      <apex:pageBlock title="Listado de Cuentas">
         <apex:pageBlockSection columns="1">
            <apex:repeat value="{!records}" var="acc">
               <apex:outputField value="{!acc.Name}"/>
            </apex:repeat>
         </apex:pageBlockSection>
      </apex:pageBlock>
   </apex:form>
</apex:page>

Siendo este el resultado

Captura de pantalla 2016-11-07 a las 18.52.19.png

Controlador Personalizado

¿Y cómo puedo conseguir lo mismo pero con un controlador creador por mi mismo? Sólo tienes que seguir estos pasos:

Crear un Controlador en Apex

Una clase en Apex sencilla. Pero ten en cuenta lo que explicamos en el último post sobre nomenclatura, sharings, etc. Lo único que debemos tener en cuenta es el constructor. Esta vez, en vez de utilizar ApexPages.StandardController, usaremos la clase ApexPages.StandardSetController que nos permitirá recuperar la lista de registros seleccionados (por ejemplo) con el método getRecords de forma similar que el controlador de único registro nos ofrecía el método getRecord. Pero además, en este caso, tenemos métodos para facilitar la paginación cómo getHasNext ó getHasPrevious.

public with sharing class AccountListController
{
   public AccountListController(ApexPages.StandardSetController controller)
   {
      //TODO añadir código
   }
}

Añadir método para recuperar la lista de cuentas

Muy sencillo. Añadimos la variable registroCuentas con sus métodos get y set. Por otro lado, añadimos un método que recuperará las listas que tenemos en base de datos por medio de una SOQL calcularCuentas. Y por último, en el constructor, rellenamos la variable privada con la información.

public with sharing class AccountListController
{
   private List<Account> registroCuentas;
 
   public AccountListController(ApexPages.StandardSetController controller)
   {
      setRegistroCuentas(calcularCuentas());
   }
 
   public List<Account> calcularCuentas()
   {
      try
      {
         return [Select Id, Name From Account];
      }
      catch(Exception ex)
      {
         ApexPages.addMessages(ex);
         return null;
      }
   }
 
   public List<Account> getRegistroCuentas()
   {
      return registroCuentas;
   }
 
   public void setRegistroCuentas(List<Account> value)
   {
      registroCuentas = value;
   }
}

Modificar la página para usar el Controlador

En este último paso sólo tenemos que añadir el atributo extensions cuyo valor es el nombre del controlador personalizado que hemos creado. Además, debemos usar nuestro método como valor en el atributo recordSetVar.

<apex:page standardController="Account" 
           extensions="AccountListController" 
           recordsetVar="registroCuentas">
   <apex:form >
      <apex:pageBlock title="Listado de Cuentas Personalizado">
         <apex:pageBlockSection columns="1">
            <apex:repeat value="{!registroCuentas}" var="acc">
               <apex:outputField value="{!acc.Name}"/>
            </apex:repeat>
         </apex:pageBlockSection>
      </apex:pageBlock>
   </apex:form>
</apex:page>

captura-de-pantalla-2016-11-07-a-las-19-23-30

Como puedes ver, el look & feel de la página es igual que la página que usa el controlador estándar.

A partir de ahí, eres tu quien pone los límites en una página.

Alternativa

Por último, si nuestro caso de uso es simplemente mostrar la lista de Cuentas, pero no nos piden nada más (por ejemplo mostrar sólo unos campos), podemos usar el tag apex:enhancedList y no reinventar la rueda. Con esta línea de código, podemos mostrar todas las cuentas de nuestra organización junto con sus enlaces de editar, borrar, seleccionar, etc y sin ningún esfuerzo.

<apex:page >
   <apex:enhancedList type="Account" 
                      height="300" 
                      rowsPerPage="10" 
                      id="AccountList" />
</apex:page>

 

captura-de-pantalla-2016-11-08-a-las-17-45-17

Esta ha sido una entrada sencilla en la que hemos aprendido

  • Mostrar una lista de Cuentas por medio del controlador Standard
  • Mostrar una lista de Cuentas via un controlador personalizado
  • Mostrar una lista de Cuentas usando el tag apex:enhancedList 

Nos vemos en el próximo post.