Apex Invocable Actions

                       Let us discuss here how to use apex action. Action encapsulate a piece of logic that allows a user to perform some work, such as sending the email. Use actions to add more functionality to your applications. Choose from standard actions, such as posting to Chatter or sending email, or create actions based on your company’s needs. To run the apex invocable actions you need to use  InvocableMethod Annotation. Invocable methods are called with the REST API and used to invoke a single Apex method. Invocable methods have dynamic input and output values and support describe calls. The following code sample shows an invocable method with primitive data types. Represents a standard or custom invocable action.

public class AccountQueryAction {
    @InvocableMethod(label='Get Account Names' description='Returns the list of account names corresponding to the specified account IDs.')
    public static List<String> getAccountNames(List<ID> ids) {
        List<String> accountNames = new List<String>();
        List<Account> accounts = [SELECT Name FROM Account WHERE Id in :ids];
        for (Account account : accounts) {
            accountNames.add(account.Name);
        }
        return accountNames;
    }
}

Calling as an Apex Actions

Now you can able to call the apex class from the apex action using the rest API resource.use the following rest API resource.
/vXX.X/actions/custom/apex
/vXX.X/actions/custom/apex/action_name

Now login into the workbench and run the following rest API using the rest utils. This example invokes the Apex action called AccountQueryAction , which takes a list of IDs and returns a list of associated account names. The top-level key name in the JSON request body must be inputted.

Note The resource is the name of the Apex class, not the Apex method. 

here is the request body for the post method

{
   "inputs":[
      {
         "ids":"0011U000005hPg5QAE"
      },
      {
         "ids":"0011U000005hPg0QAE"
      },
      {
         "ids":"0011U000005hPgBQAU"
      }
   ]
}

After executing the rest API you can able to see the response as shown below.

Quick Notes 

  1. Describe and invoke for an Apex action respect the profile access for the Apex class. If you don’t have access, an error is issued.
  2. If you add an Apex action to a flow and then remove the @InvocableMethod annotation from the Apex class, you get a run-time error in the flow.
  3. If an Apex action is used in a flow, packageable components that reference these elements aren’t automatically included in the package.
  4. An Apex invocable action can be declared public or global in a managed package. However, that action doesn’t appear in the Cloud Flow Designer’s list of available actions while building or editing a flow. These invocable actions can still be referred to by flows within the same managed package. Global Apex invocable actions in a managed package can be used in flows outside the managed package, anywhere in the organization, and appear in the Cloud Flow Designer’s list of available actions to add to a flow.

Refer this links for more information on input and output variable support

https://developer.salesforce.com/docs/atlas.en-us.api_action.meta/api_action/actions_obj_apex.htm

https://developer.salesforce.com/docs/atlas.en-us.216.0.apexcode.meta/apexcode/apex_classes_annotation_InvocableMethod.htm

 

 

Flow Invocable Actions

In this post, I will show how to use the flow Invocable Action… Actions are easy to discover and use, and also easy to understand and implement. Every button and link in Salesforce can be considered an action. A consistent Actions API and framework support the creation and distributed use of actions throughout Salesforce. Actions are available in the REST API. Invocable actions, also known as dynamic actions, can be invoked from a common endpoint in the REST API. Here we will be creating a flow and calling the flow using the REST API. The example we are taking here is returning the order and order lines items based on the order name from the auto-launched flow.

1. Create a flow 

now create a flow that will take the input argument from the rest API and return query order and order line items based on the order name.now from the flow designer create a new input variable that will collect the order name.

 

Drag and drop the fast lookup on the flow builder and Lookup the order name by passing the Order name from the inputVariable created above

OrderResult is sobject variable and adds the following fields

Now drag and drop one more Fast lookup now to query the order line items and this time we will assign the value to result variable to sobject collections.

Orderlines in the sobject collection type and return the following fields.

Connect the two fast lookups as shown below

Now save the flow of type Auto launched Flow as shown below

2. Calling Flow Actions

Now you will be able to call the flow from the invocable flow actions. Now you need to use the custom flow actions rest URI

/vXX.X/actions/custom/flow

Invokes the OrderAPIService flow

/vXX.X/actions/custom/flow/OrderAPIService

You will be able to execute the REST API from the workbench. Go to the salesforce workbench and execute the rest API as shown below

After executing the flow action, you can able to see the result as shown below

Understanding Input and Output

        Input values vary according to the input variables specified for each flow. For autolaunched flows, the input values vary according to the input variables in that flow. In this example the input variable is OrderNumber Invocable processes always require either one of the following input parameters:

  • sObjectId: The Id of the sObject record that you want the process to execute on. The record must be of the same object type as the one on which the process is defined.
  • sObject: The sObject itself that you want the process to execute on. The sObject must be of the same object type as the one on which the process is defined.
{
   "inputs":[
      {
         "OrderNumber":"00000100"
      }
   ]
}

Output values vary according to the output variables specified
Flow__InterviewStatus is available for flows. Invocable processes do not have outputs. In the response, you can able to see the Flow__InterviewStatus as finished.

 

Lightning Component Quick Actions

Introductions

In this blog, I am going to explain how to custom actions to customize lightning and  Salesforce1 experiences with both global actions and quick actions. Global actions are not associated with any object means you can insert data that is not going to establish any relations other record and Object-specific actions are associated with the object so it allows creating records that have automatic relationships to other records, make updates to specific records. To available Lightning component as actions, you need to implement either the force: LightningQuickAction or force: LightningQuickActionWithoutHeader interfaces.Add the force:hasRecordId interface to a Lightning component to enable the component to be assigned the ID of the current record. When we implement the force:lightningQuickAction interface display in a panel with standard action controls, such as a Cancel button. If instead, you want complete control over the user interface, use the force: lightningQuickActionWithoutHeader interface. Components that implement the force:lightningQuickActionWithoutHeader display in a panel without additional controls and are expected to provide a complete user interface for the action.

Object-Specific Actions
What we are going to do it here, we will create a quick action that is going to create a quote from the opportunity. Create an apex class as shown below and the apex class is used to communicate to server-side actions to save the Quote.

public class QuickQuote {
    @AuraEnabled
    public static boolean saveQuote(Quote newQuote, Id oppId) {
        system.debug('newQuote'+newQuote);
        newQuote.OpportunityId = oppId;
        try{
            insert newQuote ; 
            return true ;
        }catch(Exception e){
            throw new AuraHandledException(e.getMessage());
            return false ;
        }
        
    }
    
}

Go to developer console and create a new lightning component name ” Quick Quote” with below code

<aura:component controller="QuickQuote"
                implements="force:lightningQuickActionWithoutHeader">
    <aura:attribute name="recordId" type="String" />
    <aura:attribute name="newQuote" type="Quote"
                    default="{ 'sobjectType': 'Quote','Name':'Default Quote' , 
                             'ExpirationDate':'2017-10-30','Email':'test@email.com' ,'Phone':'' ,'Discount':'19' , 'BillingName':'Teest' }" /> 
    <div class="slds-page-header" role="banner">
        <h1 class="slds-page-header__title slds-m-right--small
                   slds-truncate slds-align-left">Create New Quote</h1>
    </div>
    <lightning:input aura:id="quoteName" 
                     name="Quote Name" 
                     label="Quote Name" 
                     value="{!v.newQuote.QuoteToName}"
                     required="true"/>
    <ui:inputDate aura:id="quoteDate" displayDatePicker="true"
                  label="Expiration Date" 
                  value="{!v.newQuote.ExpirationDate}"
                  />
    <lightning:input aura:id="quoteEmail"
                     name="Email Date" 
                     label="Email"
                     value="{!v.newQuote.Email}" 
                     Type="email"/>
    <lightning:input aura:id="quotePhone" 
                     name="Phone Date" 
                     label="Phone"
                     value="{!v.newQuote.Phone}"
                     Type="tel"/>
    
    <lightning:input aura:id="quoteDiscount" 
                     name="Discount" 
                     label="Discount"
                     value="{!v.newQuote.Discount}"
                     Type="number"/>
    
    <lightning:button label="Create Quote" onclick="{!c.createQuote}"
                      class="slds-m-top--medium"/>
    
    <lightning:button label="Cancel" onclick="{!c.handleCancel}" class="slds-m-top--medium" />
    
    
</aura:component>

Here is Lightning component controller code.

({
    createQuote: function(component, event, helper) {
        var saveAction = component.get("c.saveQuote");
        console.log('saveAction');
        saveAction.setParams({
            newQuote: component.get("v.newQuote"),
            oppId: component.get("v.recordId")  
             
        });
        saveAction.setCallback(this, function(response) {
            var state = response.getState();
            console.log('state'+state);
            if(state === "SUCCESS") {
                var resultsToast = $A.get("e.force:showToast");
                resultsToast.setParams({
                    "title": "Quote",
                    "message": "Quote Creation is Success."
                });
                $A.get("e.force:closeQuickAction").fire();
                resultsToast.fire();
                $A.get("e.force:refreshView").fire();
            }
            else if (state === "ERROR") {
                var errors = response.getError();
                if (errors) {
                    if (errors[0] && errors[0].message) {
                        console.log("Error message: " + 
                                    errors[0].message);
                    }
                } else {
                    console.log("Unknown error");
                }
            } 
        });
        console.log('En Quier action ');
        $A.enqueueAction(saveAction);
        
        
    },
    
    handleCancel: function(component, event, helper) {
        $A.get("e.force:closeQuickAction").fire();
    }
})

The createQuote action handler method will do the following action

  • Invoke server action to save the quote by calling component.get(“c.saveQuote”) from the apex class .
  • Defines a callback function to handle the server response.
  • Enqueues the server action by calling” $A.enqueueAction(saveAction)
  • handleCancel Closes the action panel by firing the force:closeQuickAction event.
  • Displays a “toast” message that the Quote was created by firing the force:showToast event.

Go to Opportunity and create an action as shown below.

Add it to the opportunity  Salesforce1 and Lightning Experience Actions section on the page layouts to create quote by using actions.

 

Global Actions

Now we are going to create global actions that allow users to create the new user from the global action.
create a new apex class “CreateNewUser” with the below code.

public class CreateNewUser {
    @AuraEnabled
    public static void insertNewUser(User userInsert ){
        try{
            
            User temp = userInsert ; 
            Database.DMLOptions dmo = new Database.DMLOptions();
            dmo.EmailHeader.triggerUserEmail = false;
            String profileId =[Select id from Profile where name = 'Chatter Free User' Limit 1] .Id;
            temp.ProfileId = ProfileId;
            temp.TimeZoneSidKey = 'America/Los_Angeles';
            temp.LanguageLocaleKey = 'en_US';
            temp.EmailEncodingKey ='UTF-8';
            temp.LocaleSidKey = 'en_US';
            temp.setOptions(dmo);
        
            insert temp ;  
        }catch(Exception e){
            throw new AuraHandledException(e.getMessage());
          }
        
    }
    
}

Go to developer console and create a new lightning component “CreateNewUser” with below code.

<aura:component controller="CreateNewUser" implements="force:lightningQuickActionWithoutHeader">
    
    
    <aura:attribute name="newUser" type="User"  default="{ 'sobjectType': 'User',
                                                         'FirstName':'' , 
                                                         'LastName':'',
                                                         'Email':'' ,
                                                         'Username':'' ,
                                                         'Alias':''
                                                         }" />
    
    
    <ui:inputtext label="First Name" value="{!v.newUser.FirstName}"/>
    <ui:inputtext label="Last Name" value="{!v.newUser.LastName}"/>
    <ui:inputemail label="Email" value="{!v.newUser.Email}"/>
    <ui:inputemail label="User Name" value="{!v.newUser.Username}"/>
    <ui:inputemail label="Alias" value="{!v.newUser.Alias}"/>
    
    <lightning:button label="Save " onclick="{!c.handleSave}"
                      variant="brand" class="slds-button slds-button_brand slds-col_bump-left"/>
    
    
       
</aura:component>

Here is the component controller.

({
    handleSave: function(component, event, helper) {
        
        var newUserInsert = component.get("c.insertNewUser");
        //console.log(newUserInsert)
        console.log(component.get("v.newUser"));
        newUserInsert.setParams({
            "userInsert": component.get("v.newUser")
        });
        
        console.log(newUserInsert);
        newUserInsert.setCallback(this, function(response) {
            console.log(response);
            var state = response.getState();
            if(state === "SUCCESS") {
                // Prepare a toast UI message
                var resultsToast = $A.get("e.force:showToast");
                resultsToast.setParams({
                    "title": "Record is  Created  !",
                    "message": response
                });
                // Update the UI: close panel, show toast, refresh account page
                $A.get("e.force:closeQuickAction").fire();
                resultsToast.fire();
                $A.get("e.force:refreshView").fire();
            }
            else if (state === "ERROR") {
                var errors = response.getError();
                if (errors) {
                    if (errors[0] && errors[0].message) {
                        console.log("Error message: " + 
                                    errors[0].message);
                    }
                } else {
                    console.log("Unknown error");
                }
            } 
                else {
                    console.log('Unknown problem, response state: ' + state);
                }
        });
        
        // Send the request to create the new contact
        $A.enqueueAction(newUserInsert);
    }
    
    
},
 
 })

Go to Global action from the setup menu and create a new global action as shown below.

Go to publisher layout from the setup menu and add the lightning component create to the layout as shown below.

You can access global action from salesforce global header.

 

 

 

 

 

 

 

Visualforce Page Actions

Introduction

In this blog post, I am going to explain how to set up global actions and object-specific actions by using visualforce page.The global action which we are going to see in this blog is to send the message to slack channel and object specific action is to take the case ownership from the assigned queues.

Global Actions:-

As we are going to going to post the message to slack by using the global action, go and create a Slack app and get the slack token. Please refer this link

visualforce page is here below

<apex:page controller="GlobalSlackActions" lightningStylesheets="true" sidebar="false" showQuickActionVfHeader="false" docType="html-5.0">
    <apex:slds />
    /canvas/sdk/js/publisher.js
    
    <div class="slds-scope">
        <apex:form >
            <apex:actionFunction action="{!postMessagetoSlack}" name="postSlackMessage" rerender="out"
                                 oncomplete="refreshFeed();"/>
            <apex:outputPanel id="out">
                
                
                <div class="slds-form--compound slds-m-top--xxx-small">
                    <div class="slds-form-element">
                        <div class="slds-form-element__row slds-align_absolute-center">
                            <label class="slds-form-element__label" for="sample1">#Slack Channel</label>
                            <div class="slds-form-element__control slds-picklist">
                                <apex:selectList id="countries" value="{!slackChannel}" size="1" required="true" styleClass="slds-input">
                                    <apex:selectOptions value="{!ListOfSlackChannels}"/>
                                </apex:selectList>  
                            </div>
                        </div>
                    </div>
                    <div class="slds-form-element">
                        
                        <div class="slds-form-element__row slds-align_absolute-center ">
                            <label class="slds-form-element__label " for="sample2"> Name </label>
                            <div class="slds-form-element__control slds-picklist">
                                <apex:inputTextArea value="{!slackTextMessge}"  id="sample1" styleClass="slds-input"/>
                            </div>
                        </div>       
                    </div>
                </div>
                
                
            </apex:outputPanel>
            <div class="slds-form-element slds-align_absolute-center">
                <button  onclick="postSlackMessage();" styleClass="slds-button slds-button_neutral">Post to Slack </button>
                <button  onclick="closeAction();" styleClass="slds-button slds-button_neutral">Close  </button>
                
            </div>
        </apex:form>
    </div>
</apex:page>

 

And the controller is here.

public class GlobalSlackActions {
    public String slackChannel {get;set;}
    public List<SelectOption> channelNameOptions{get;set;}
    public String slackTextMessge {get;set;}
    public String slackToken {get;set;}
    public GlobalSlackActions(){
        slackToken ='xoxp-167092741267-167199659508-179751455458-6fd12a391bd432b735b4efcb1313193f';
    }
    public List<SelectOption> getListOfSlackChannels () {
        String slackEndpoint = 'https://slack.com/api/channels.list?token='+slackToken;
        HttpResponse res = buildHttp(slackEndpoint);
        
        if(res.getStatusCode() ==200){
            JSON2Apex cls =  (JSON2Apex)parse(res.getBody());
            list<Channels> c =cls.channels ; 
            System.debug('c'+c);
            List<SelectOption> options = new List<SelectOption>();
            for(Channels cName :c){
                options.add(new SelectOption(cName.name,cName.name));
            }
            return options ; 
        }else{
            return null ;
        }
    }
    
    
    
    public class JSON2Apex {
        public Boolean ok;
        public List<Channels> channels{get;set;}
    }
    
    
    
    public class Channels {
        public String id;
        public String name{get;set;}
        public Boolean is_channel;
        public Integer created;
        public String creator;
        public Boolean is_archived;
        public Boolean is_general;
        public Integer unlinked;
        public String name_normalized;
        public Boolean is_shared;
        public Boolean is_org_shared;
        public Boolean is_member;
        public Boolean is_private;
        public Boolean is_mpim;
        public List<String> members;
        public Integer num_members;
    }
    
    
    public static JSON2Apex parse(String json) {
        return (JSON2Apex) System.JSON.deserialize(json, JSON2Apex.class);
    }
    
    
    public PageReference postMessagetoSlack () {
        String slackEndpoint = 'https://slack.com/api/chat.postMessage?token='+slackToken+'&channel='+slackChannel+'&text='+slackTextMessge+'&pretty=1';
        buildHttp(slackEndpoint);
        return null ;
    }
    public HttpResponse buildHttp(String url){
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint(url);
        req.setMethod('POST');
        HttpResponse res = new HttpResponse(); 
        res = h.send(req);
        return res ;
    }
    
}

Go to set up, find global actions and create a new one as shown below.

Add to the publisher layout as shown below.

After saving the publishers layout you can able to see the quick actions as shown below.

Object Specific actions

Now I am going to walk thru how to create the object specific action.To associate an action to object you need to user standardController attribute of the object. Here we are going to create a Case Quick actions which are allowing the user to take the ownership of the case from case queue.

Here is the visual force page

<apex:page standardController="Case" extensions="CaseTakeOwnership" showQuickActionVfHeader="false">
    
    /canvas/sdk/js/publisher.js
    
    
    <apex:form id="demo">
        <apex:actionFunction action="{!takeOwnership}" name="takeOwner" reRender="demo" oncomplete="refreshFeed();"/>
        <div style="align:center">
            <br/><br/><br/>
            <button  onclick="takeOwner();" class="sucessBtn">Take Ownership</button> <br/><br/><br/><br/>
            <button onclick="closeAction();" class="closebtn">Cancel</button>
        </div>
    </apex:form>
    
    
</apex:page>

The controller is here below.

public class CaseTakeOwnership {
    public Case careRecord {get; set;}
    public CaseTakeOwnership(ApexPages.StandardController controller) {
        careRecord =(Case)controller.getRecord();
    }
    public PageReference takeOwnership() {
        try {
            careRecord.OwnerId = UserInfo.getUserId();
            update careRecord;
        } catch(Exception ex){
        }
        return null ;
    }
    
    
}

Both in global action and Object actions, I set showQuickActionVfHeader to false. So that we will not able to see the Standard Submit and Cancel Global action header.

Now go to Case Object buttons and links and create a new action as shown below

Add the object to quick action, salesforce 1 and lightning action on page layout based on whether you wanted to show on salesforce class or salesforce 1 as shown below.

 

Publisher Events

Refresh feed (desktop) 

Sfdc.canvas.publisher.publish({ name: 'publisher.refresh',
 payload: {feed:true}
});

Submit hook (Salesforce1)

Sfdc.canvas.publisher.subscribe({name: "publisher.showPanel",
 onData:function(e) {
 Sfdc.canvas.publisher.publish({name:
 "publisher.setValidForSubmit", payload:"true"});
}});
Sfdc.canvas.publisher.subscribe({ name: "publisher.post",
 onData: function(e) {
 alert("call some remote action here");
 Sfdc.canvas.publisher.publish({ name: "publisher.close",
 payload:{ refresh:"true" }});
}});

Close publisher event

        Sfdc.canvas.publisher.publish({name: "publisher.close", payload:{ refresh:"true" , successMessage: 'Cancel!'}});  

Activate publish button 

Sfdc.canvas.publisher.publish({
    name: "publisher.setValidForSubmit", 
    payload:"true"});
Clear Panel State
Sfdc.canvas.publisher.subscribe(
             { name : "publisher.clearPanelState", onData:function(e) { alert('Fire ClearPanelState'); }}
        );

Show Panel State 

   Sfdc.canvas.publisher.subscribe(
             { name : "publisher.showPanel", onData:function(e) { alert('Fire ShowPanel'); }}
        );
Success State 
            Sfdc.canvas.publisher.publish({ name : "publisher.success", payload : { feed:true }});

 

Redirect (Salesforce1)

sforce.one.navigateToSObject(recordId,view)
sforce.one.navigateToURL(url)
sforce.one.navigateToFeed(subjectId, type)
sforce.one.navigateToFeedItemDetail(feedItemID)
sforce.one.navigateToRelatedList(relatedListId, parentRecordId)
sforce.one.navigateToList(listViewId, listViewName, scope)
sforce.one.createRecord(entityName, recordTypeId)
sforce.one.editRecord(recordId)