Friday, May 15, 2020

Mention a User in Chatter Post and Set Chatter Post from a Specific User through Apex Code

Use case:
Business requires when a new account is created with account type is partner or vendor, a chatter post is added to the Account page. The chatter user must be a specific user and chatter post must mention the department director.

Analysis:
1. A trigger is added to Account object.
2. How to add the required chatter post using Apex code is a bit complicated.
There are two points:
2.1. How to add a specific user as the chatter post created by user through Apex Code?
2. 2. How to mention a user in chatter post through Apex Code

We have two ways to add a chatter post, using FeedItem object and using ConnectAPI name space classes.

With FeedItem object we can assigned CreatedById when the record is created. It meets the requirement item 2.1. But it cannot mention a user.

With ConnectAPI name space classes, it is the other way. It can mention a user but cannot assign createdbyId.


Solution:
1. Add a trigger on Account object, call the method in step 2.

PostFeedItem(accountId,mentionedUserId,'Please have a look?   (--by autobot--)');

Please note '(--by autobot--)', this will be used to identify if the chatter post is created by apex code or real person input. This text will be used in the code below.

2. Create a method using ConnectAPI to generate chatter post.
This method has three parameters: parent record Id, mentioned User Id and the message.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public static void PostFeedItem(Id ParentId,Id mentionedUserId, string message)
{
    ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
    ConnectApi.MentionSegmentInput mentionSegmentInput = new ConnectApi.MentionSegmentInput();
    ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
    ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
    messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
    mentionSegmentInput.id = mentionedUserId;
    messageBodyInput.messageSegments.add(mentionSegmentInput);
    textSegmentInput.text = message;
    messageBodyInput.messageSegments.add(textSegmentInput);
    feedItemInput.body = messageBodyInput;
    feedItemInput.feedElementType = ConnectApi.FeedElementType.FeedItem;
    feedItemInput.subjectId = ParentId;
    ConnectApi.FeedElement feedElement =
    ConnectApi.ChatterFeeds.postFeedElement(null, feedItemInput);
}


3. Add a trigger on FeedItem, when a new FeedItem record is created, change the createdbyId.
In the trigger, it take text '(--by autobot--)' as identifier if createdbyid needs to be assigned to a specific user.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
trigger FeedItemTrigger on FeedItem (before insert) {

    List<FeedItem> chatterPosts = new List<FeedItem>();
    for(FeedItem chatterPost : trigger.new)
    {
        if (chatterPost.body.contains('(--by autobot--)'))
        {
            chatterPosts.add(chatterPost);
        }
    }
    FeedItemTriggerHanlder(chatterPosts);
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public with sharing class FeedItemTriggerHanlder {
    public static void UpdateCreatedById(List<FeedItem> chatterPosts) {
        Id userId = [SELECT Id FROM User where isActive=true AND userName = 'xxxxx' LIMIT 1].Id;
        for (FeedItem chatterPost : chatterPosts)
        {
            chatterPost.createdById = userId;
        }

    }
}


How to use the code

You need to copy the code above in design section 2 and 3 and build you own Account trigger. As a reminder, it should be a AFTER trigger.





Saturday, May 9, 2020

Five Easy Steps to Implement Autocomplete in Salesforce Visual Force Page


Almost all website uses Autocomplete function when doing search. Autocomplete gives users suggestions on what you could be searching for.

For example, if you type “sales” in Google search box, you can see a list of most popular searched words or sentences listed under the search box; if one of them is what you want to search, just move mouse to the word and click it. This saves you time and helps you avoiding typo.

When doing Visual Force page design, developers find Salesforce does not provide native autocomplete function.


In this blog, I will how to implement Autocomplete in Visual Force Page with the help of Jquery UI STEP BY STEP.

If you don't  have time to read the details of the implementation, do the following and your code should work.

  • Copy the code of step 1, 3
  • Refer to Step 2, find search input text <apex:inputText> component in your Visual force Page, add class "contact_search" to this component, and copy line 4 of section 3 to your code.
  • Copy code of step 4, change line 31 of code in section 4 to your own Apex class method.
  • Refer to step 5, make sure your Apex Class method works. (You can't copy the code for Apex method. 😃



1. Add JQuery JavaScript library and css Style to Visual Force Page
Within Visual Force Page, this can be done by referencing static resource or simply by referencing the library through <script> tag. I prefer just add the line right after <apex:page>  

Style ui-autocomplete will be used in displaying the autocomplete list. It is very important.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<apex:Page controller="customController">
    <style type="text/css">
    .ui-autocomplete {
        max-height: 80%;
        overflow-y: auto;
        overflow-x: hidden;
        min-width: 250px;
    }
    </style>
    <script src="https://code.jquery.com/jquery-1.8.2.js"></script>
    <script src="https://code.jquery.com/ui/1.9.0/jquery-ui.js"></script>
    <link rel="stylesheet" href="https://code.jquery.com/ui/1.9.1/themes/base/jquery-ui.css"/>
      ......


2. Add <apex:ActionSupport> component to search <apex:inputText> component.

The following code present user an input text box and a search button.



1
2
3
4
5
<apex:outputPanel id="searchPanel">
    <legend class="slds-form-element__legend slds-form-element__label ">Search</legend>
    <apex:inputText value="{!searchString}" styleClass="slds-m-right_x-small"/>
    <apex:commandButton action="{!search}" value="Search" reRender="formId" status="spinner"  id="searchButtonId" />
</apex:outputPanel>
Just add <apex:actionSupport> to <apex:inputText> component and add class “contact_search” to <apex:inputText> component.
Note:  “contact_search” will be used in JQuery to identify which input text box to have list value populated.

1
2
3
4
5
6
7
<apex:outputPanel id="searchPanel">
    <legend class="slds-form-element__legend slds-form-element__label ">Search</legend>
    <apex:inputText value="{!searchString}" styleClass="contact_search slds-m-right_x-small">
        <apex:actionSupport event="onchange" rerender="refreshAutocomplete"/>
    </apex:inputText>
    <apex:commandButton action="{!search}" value="Search" reRender="formId" status="spinner"  id="searchButtonId" />
</apex:outputPanel>


3. Add <apex:actionFunction> component called “refreshAutocomplete”.
<apex:actionFunction component that provides support for invoking controller action methods directly from JavaScript code using an AJAX request.

You can add the following line to line just right after <apex:form>


1
2
3
<apex:form id="formId">
   <apex:actionFunction name="refreshAutocomplete" id="refreshAutocomplete" 
    rerender="autocompletePanel"/>


In step 2, input text box change triggers AJAX request refreshing component id “refreshAutocomplete”.
So we add a <apex:actionFunction> with id=”refreshAutocomplete”. When this component refreshes, it will rerender component “autocompltePanel


4.Add <apex:outputPane id=”autocompletePanel”> with JQuery JavaScript functions.
Copy the following line to the end of your VF page. It could be out of </apex:form> tag.
This is the core function of autocomplete with JQuery. I will explain line by line after JavaScript code.

NoteautocompletePanel, it matches the one in step 3.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    <apex:outputPanel id="autocompletePanel">
        <script type="text/javascript">

            $(function() {
                try {
                    var ac = $( "[class*='contact_search']" ).autocomplete({
                        minLength: 2,
                        source: function(request, callback){
                            var term = request.term;
                            queryContactCallback(term, callback);
                        },
                        select: function( event, ui ) {
                            $( "input[class*='contact_search']" ).val(ui.item.label);
                            return false;
                        }
                    }).data( "ui-autocomplete" )._renderItem = function( ul, item ) {
                        return $( "<li>" )
                        .append( "<a class='slds-text-title'>"+ item.label + "</a>" )
                        .appendTo( ul );
                    };
                } catch(e) {
                    console.log('error:' + e);
                 }
            });

            function queryContactCallback(query, callback)
            {
                var data = [];

                Visualforce.remoting.Manager.invokeAction(
                    '{!$RemoteAction.COT_ContactController.getAutoCompleteContact}',
                    query,
                    function(lstresult, event){
                        for (var n=0; n<lstresult.length; n++){
                            var result = {
                                value: lstresult[n],
                                label: lstresult[n]
                            };
                            data.push(result);
                        }
                        callback(data);
                    }
                );

            }

        </script>
    </apex:outputPanel>

Line 6 : var ac = $( "[class*='contact_search']" ).autocomplete({
Make sure the class value matches the input text class added in step 2. It is ‘contact_search’.

Line 7: minLenght: 2
This line means only when there are at least to characters entered in the box, the Remote call will be made to the controller.

Line 9: var term = request.term;
requet.term is what user enters in input text box.

Line 10: queryContactCallback(term, callback);
This lines calls JavaScript function queryContactCallback.  queryContactCallback is function at line 26.

Line 12 to Line 14:
            select: function( event, ui ) {
                            $( "input[class*='contact_search']" ).val(ui.item.label);
                            return false;
                        }
         
This four lines handle when user select a value from autocomplete dropdown list.

It basically gets the item label and put in input text box.



Line 16 to Line 19: 
            data( "ui-autocomplete" )._renderItem = function( ul, item ) {
                    return $( "<li>" )
                    .append( "<a class='slds-text-title'>"+ item.label + "</a>" )
                   .appendTo( ul );

Do you still remember "ui-autocomplete"?  We added it to VF Page in step 1.  Yes, it is used in line 16.
Line 16 to Line 19 get the return values from controller method and populate into a list. If needed, developer can modify this line .append( "<a class='slds-text-title'>"+ item.label + "</a>" )  to make the dropdown more fancy.

Line 26: Function queryContactCallback
This method calls the controller method and return list value.
{!$RemoteAction.customController.getAutoCompleteContact}',
This line tells Salesforce to call method getAutoCompleteContract in Apex Class called “customController”.
If there are multiple parameters passing to the method, you can do like this:

            Visualforce.remoting.Manager.invokeAction(
                        {!$RemoteAction.customController.getAutoCompleteContact}',
                        parameter1,
                        parameter2,
                        function(lstresult, event){

Line 34 to Line 39: Return values from Controller method call
It iterates the return list value from controller and put in list. 
You can define your own attribute by replacing value, label.
In the following example, you can set attribute to firstname and lastname, but make sure in selection function Line 13, the right attribute is referenced, change ui.item.label to the right attribute ui.item.firstname.


1
2
3
4
5
6
7
 for (var n=0; n<lstresult.length; n++){
     var result = {
         firstname: lstresult[n],
         lastname: lstresult[n]
     };
     data.push(result);
 }

5.Add Controller Method

This is standard salesforce method. Don’t forget adding @RemoteAction annotation and setting method as Static.





1
2
3
4
5
6
7
8
9
    @RemoteAction
    public Static List<String> getAutoCompleteContact(String searchString)
    {
        List<String> result = new List<String>();

        ....

        Return return;
    }

Let's take a look how autocomplete looks like. Look pretty good!



Sunday, April 26, 2020

Salesforce - Custom Duplicate Job to Handle Millions of Records

Duplicate Job in Lightning Experience is a convenient tool to find duplicate business and personal accounts, contacts, and leads in Salesforce. 

An administrator can easily create a Duplicate Job against a matching rule and let it run. The results are then added to the Duplicate Record Set and users can find these results through the Duplicate Record Set tab.

However, Salesforce Duplicate Job has some limitations. One of the major issues is that when total duplicates found exceed 1,000,000 records, the job will abort.

One of my clients came across this issue. They had more than 10 million Lead records, since records were imported from different legacy systems. From a business perspective, it is obvious that there would be a huge number of duplicates. When using the Salesforce Duplicate Job, it processed all records in one process and it failed because of  the 1,000,000 duplicates limitation.

To solve this problem, I helped my client build a custom Duplicate Job application using the Apex class. 
This application returns the same results as the Salesforce Duplicate Job, but it can segment the records by using a query. This avoids the 1,000,000 duplicates limitation.


How to use it:
1. Open Anonymous Window in Developer Console.
2. Run the following script


string queryString = 'Select Id from Contact where lastname like 'a%\'';   //query

string objectName = 'Contact';   //object name

ID batchprocessid = Database.executeBatch(
         new FindDuplicateRecordsBatchClass(queryString,objectName),200);


3.Monitor the process in Apex Job
                               
Possible Issues and solution:
It may have Batch failure because of hitting DML or Query record governor limit. This only happens when batch scope size is more than 300. This problem can be fixed by decreasing the scope size in the above anonymous script..

Feel free to use the source code below.


/**
* @File Name            :   FindDuplicateRecordsBatchClass
* @Description          :   This tool is designed to replace Salesforce built-in Duplicate Job.
                            Salesfore build-in Duplicate Job cannot handle more than 1 millions records and Shaw has more than 8+ Contact records.
* @Author               :   David Zhu 
* @Group                :   Apex Tool
* @Last Modified by     :   David Zhu
* @Last Modified time   :   2020-03-27
* @Modification Log     :
* @Tool Instructions    :   This tool can be used for checking duplicate records for any object.
                            It does not modify object or update record.
                            It requires a matching rule on the desired object and a duplicate rule which uses the matching rule.
                            When it runs, duplicate records will be saved to Duplicate Record Set Object. The same way build-in Duplicagte Job does.

                            Run this as anonymous Apex, make sure SOQL query returns less than 50+ million rows. If record is more than 50+ million, add where clause to the SOQL query string.

                            Apex Class and Parameters: FindDuplicateRecordsBatchClass(string query,string sObjectType);
                            When instanitiate FindDuplicateRecordsBatchClass, the first parameter is the SOQL query, the second parameter is object name.

                            Use the following code snippet to start the batch job and monitor job status

                                ID batchprocessid = Database.executeBatch(new FindDuplicateRecordsBatchClass('SELECT Id FROM Contact','Contact'),200);
                                System.debug('batchprocessid:' + batchprocessid);

                                AsyncApexJob aaj = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors FROM AsyncApexJob WHERE ID =: batchprocessid];
*-------------------------------------------------------------------------------------
* Ver        Date        Author        Modification
* 1.0       2020-03-27   David Zhu    Created the file/class
*/

global with sharing class FindDuplicateRecordsBatchClass  implements Database.Batchable<sObject>,Database.Stateful{

    global String query;  //If query return records are more than 50m, batch will fail right away.
    global String entity;

    global Map<String,String> ruleMap;

    global integer totalRecords;

    global FindDuplicateRecordsBatchClass(String queryString,String entityName) {
        query= queryString;
        entity = entityName;
        List<DuplicateRule> rules = [SELECT Id,developerName FROM DuplicateRule WHERE SObjectType = :entity];

        ruleMap = new Map<String,String>();

        for (DuplicateRule rule :rules) {
            ruleMap.put(rule.developerName,rule.Id);
        }
        totalRecords = 0;
    }

    global Database.QueryLocator start(Database.BatchableContext BC){

        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC, List<sObject> scope){

        List<DuplicateRecordSet> dupRecordSet = new List<DuplicateRecordSet>();
        List<DuplicateRecordSetItem> dupRecordSetItems = new List<DuplicateRecordSetItem>();
        Map<String,String> dupProcessed = new Map<String,String>();

        Map<String,String> recordsToProcess = new Map<String,String>();

        for (sObject record : scope)  {

            totalRecords++;

            if (dupProcessed.containsKey(record.Id)){
                continue;
            }

            List<sObject> sObjects = new List<sObject>();
            sObjects.add(record);
            List<Datacloud.FindDuplicatesResult> results = Datacloud.FindDuplicates.findDuplicates(sObjects);

            for (Datacloud.FindDuplicatesResult findDupeResult : results)  {

                for (Datacloud.DuplicateResult dupeResult : findDupeResult.getDuplicateResults()) {

                    String rule = dupeResult.getDuplicateRule();

                    for (Datacloud.MatchResult matchResult : dupeResult.getMatchResults()) {

                        if (matchResult.getMatchRecords().size() > 0) {

                            if (!recordsToProcess.containsKey(record.Id)){
                                recordsToProcess.put(record.Id,record.Id);
                            }

                            List<String> checkDupIds = new List<String>();
                            checkDupIds.add(record.Id);

                            for (Datacloud.MatchRecord matchRecord : matchResult.getMatchRecords()){

                                if (!recordsToProcess.containsKey(matchRecord.getRecord().Id)){
                                    recordsToProcess.put(matchRecord.getRecord().Id,matchRecord.getRecord().Id);
                                }
                            }

                            List<DuplicateRecordItem> dupRecordItems = new List<DuplicateRecordItem>();
                            String entity = matchResult.getEntityType();

                            DuplicateRecordSet record = new DuplicateRecordSet();
                            record.DuplicateRuleId = ruleMap.get(rule);
                            dupRecordSet.add(record);

                            DuplicateRecordSetItem duplicateRecordSetItem = new DuplicateRecordSetItem();
                            DuplicateRecordItem recordItem = new DuplicateRecordItem();
                            recordItem.RecordId =record.Id;
                            dupProcessed.put(record.Id,record.Id);
                            dupRecordItems.add(recordItem);

                            duplicateRecordSetItem.count = dupRecordSet.size()-1;

                            for (Datacloud.MatchRecord matchRecord : matchResult.getMatchRecords()) {

                                recordItem = new DuplicateRecordItem();
                                recordItem.RecordId =matchRecord.getRecord().Id;
                                dupRecordItems.add(recordItem);
                                dupProcessed.put(recordItem.RecordId,recordItem.RecordId);
                            }

                            duplicateRecordSetItem.duplicateRecordItems = dupRecordItems;
                            dupRecordSetItems.add(duplicateRecordSetItem);
                        }
                    }
                }
            }
        }

        if (dupRecordSet.size() > 0){

            //Retrieve all records which have been add to Duplicate Record Set
            List<DuplicateRecordItem> processedItems = new List<DuplicateRecordItem>();
            Map<String,DuplicateRecordItem> processedItemsMap = new Map<String,DuplicateRecordItem> ();

            if (recordsToProcess.size() > 0){
                processedItems= [Select Id,DuplicateRecordSetId,RecordId from DuplicateRecordItem where RecordId in :recordsToProcess.Keyset()];

                for (DuplicateRecordItem processedItem : processedItems) {
                    if (!processedItemsMap.containsKey(processedItem.recordId)) {
                        processedItemsMap.put(processedItem.recordId,processedItem);
                    }
                }
            }


            //End of retrieving

            //Remove any duplicates which have already been add to Duplicated Record Set
            List<DuplicateRecordSet> updDupRecordSet = new List<DuplicateRecordSet>();
            List<DuplicateRecordSetItem> updDupRecordSetItems = new List<DuplicateRecordSetItem>();

            for (integer i =0;i<dupRecordSet.size();i++)
            {
                DuplicateRecordSet dupRec = dupRecordSet[i];
                Boolean isExist = false;
                List<DuplicateRecordItem> updItems = new List<DuplicateRecordItem>();

                for (DuplicateRecordSetItem item : dupRecordSetItems){

                    if (isExist){
                        break;
                    }

                    if (item.count == i){

                        updItems = item.duplicateRecordItems;
                        for (DuplicateRecordItem recItem : updItems){

                            if (processedItemsMap.ContainsKey(recItem.RecordId)){
                                isExist = true;
                                break;
                            }
                        }

                    }
                }

                if (!isExist){
                    updDupRecordSet.add(dupRec);
                    DuplicateRecordSetItem updDupRecordItem = new DuplicateRecordSetItem();
                    updDupRecordItem.count = updDupRecordSet.size() - 1;
                    updDupRecordItem.duplicateRecordItems = updItems;
                    updDupRecordSetItems.add(updDupRecordItem);
                }
            }
            //End of removing

            if (updDupRecordSet.size() > 0) {
                insert updDupRecordSet;

                List<DuplicateRecordItem> updatedDuplicateRecordItems = new List<DuplicateRecordItem>();

                for (Integer i=0;i<updDupRecordSet.size();i++){

                    DuplicateRecordSet dupRec = updDupRecordSet[i];
                    for (DuplicateRecordSetItem item : updDupRecordSetItems){
                        if (item.count == i){

                            List<DuplicateRecordItem> duplicateRecordItems = item.duplicateRecordItems;

                            for (DuplicateRecordItem recItem : duplicateRecordItems){
                                recItem.DuplicateRecordSetId = dupRec.Id;

                                updatedDuplicateRecordItems.add(recItem);
                            }
                        }
                    }
                }
                upsert updatedDuplicateRecordItems;
            }
        }

     }

    global void finish(Database.BatchableContext BC){

        AsyncApexJob a = [SELECT Id, Status,ExtendedStatus,NumberOfErrors, JobItemsProcessed,TotalJobItems, CreatedBy.Email  FROM AsyncApexJob WHERE Id =:BC.getJobId()];

        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

        mail.setToAddresses(new String[] {UserInfo.getUserEmail()});
        mail.setReplyTo('no-reply@salesforce.com');
        mail.setSenderDisplayName('Batch Processing');
        mail.setSubject('Duplicate Records Batch Process Completed');
        String body = 'Batch Process has completed\r\n';
        body += 'Total Job Items Process: '+ a.TotalJobItems + '\r\n';
        body += 'Job Items Processed: ' + a.JobItemsProcessed + '\r\n';
        body += 'Total Records Processed: ' + totalRecords + '\r\n';
        body += 'Number Of Errors : ' + a.NumberOfErrors + '\r\n';

        mail.setPlainTextBody(body);

        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail },false);
    }

    public class DuplicateRecordSetItem
    {
        public Integer count {get;set;}
        public List<DuplicateRecordItem> duplicateRecordItems {get;set;}
    }
 }

Thursday, January 16, 2020

Create Modal Window In Lightning Web Component

What is a modal window?
 It creates a mode that disables the main window but keeps it visible, with the modal window as a child window in front of it.
It forces the user to interact with it before taking action on the main Window. It is also called popup window or dialog window.
It is commonly used for scenarios like confirming a deletion, set a value.



 In this post, I will demo how to create a modal window in LWC and how to extend it.

This base component is called "modalConfirmation".
It has four properties:
Title - displaying the title of popup window. i.e "Delete File?"
Message - displaying the message on popup. i.e. "Deleting a file also removes it from any records or posts it's attached to."
FirstButton - Action on the popup.  ie. "Cancel".
SecondButton.  - Action on the popup. i.e. "Delete"
For the button actions, I use Event to pass values to parent window, the actual process is in parent window.

 1. modalConfirmation.html
This is very similar to Aura Component implementation. Lightning Design System CSS is used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
    <div style="height: 640px;">
            <section role="dialog" tabindex="-1" aria-labelledby="heading" aria-modal="true" aria-describedby="content" class="slds-modal slds-fade-in-open">
                <div class="slds-modal__container">
                    <header class="slds-modal__header">
                        <button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse" title="Close" onclick={closeModal}>
                            <lightning-icon icon-name="utility:close" size="medium">
                            </lightning-icon>
                            <span class="slds-assistive-text">Close</span>
                        </button>
                        <h2 class="slds-text-heading_medium slds-hyphenate">{title}</h2>
                    </header>
                    <div class="slds-modal__content slds-p-around_medium">
                        <center><h2><b>{message}</b></h2><br/>
                        </center>
                    </div>
                    <footer class="slds-modal__footer">
                        <lightning-button label={firstButton} variant="neutral" onclick={firstButtonAction}></lightning-button>&nbsp;&nbsp;&nbsp;&nbsp;
                        <lightning-button label={secondButton} variant="brand" onclick={secondButtonAction}></lightning-button>
                    </footer>
                </div>
            </section>
            <div class="slds-backdrop slds-backdrop_open"></div>
        </div>
        
</template>

2. modalConfirmation.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { LightningElement,api } from 'lwc';

export default class ModalConfirmation extends LightningElement {
    @api message;
    @api title;
    @api firstButton;
    @api secondButton;
    
    firstButtonAction(event)
    {
        // Prevents the anchor element from navigating to a URL.
        event.preventDefault();

        const selectedEvent = new CustomEvent('firstbuttonclicked', { detail: this.firstButton});

        // Dispatches the event.
        this.dispatchEvent(selectedEvent);
    }

    secondButtonAction(event)
    {
        // Prevents the anchor element from navigating to a URL.
        event.preventDefault();

        const selectedEvent = new CustomEvent('secondbuttonclicked', { detail: this.secondButton});

        // Dispatches the event.
        this.dispatchEvent(selectedEvent);
    }

    closeModal(event)
    {
        // Prevents the anchor element from navigating to a URL.
        event.preventDefault();

        const selectedEvent = new CustomEvent('closeModal');

        // Dispatches the event.
        this.dispatchEvent(selectedEvent);
    }

}

3. modalConfirmation.js-meta.xml

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>47.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>


4. Parentwindow.xml
Attribute openModal is used to control the display of the modal window.

Each event triggered on modalConfirmation component (onfirstbuttonclicked,onsecondbuttonclicked and onclosemodal) is bind to a method in parent window.

When DELETE button on parent window is clicked,  modalConfirmation components API properties are assigned valued (Title,Message,FirstButton and SecondButton), plus openModal set to true.
This results modal window is displayed in front of the parent window.

1
2
3
4
5
6
7
<template>
    <template if:true={openModal}>
        <c-modal-confirmation title={title} message={messsage} first-button={firstButton} second-button={secondButton} onfirstbuttonclicked={firstButtonClicked} onsecondbuttonclicked={secondButtonClicked}  onclosemodal={closeModal}>
        </c-modal-confirmation>
    </template>

   ..................


5. Parentwindow.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { refreshApex } from '@salesforce/apex';
import getContentNoteList from '@salesforce/apex/ContentNoteController.getContentNoteList';
import deleteContentNote from '@salesforce/apex/ContentNoteController.deleteContentNote';
import removeContentNote from '@salesforce/apex/ContentNoteController.removeContentNote';
import getContentNote from '@salesforce/apex/ContentNoteController.getContentNote';

export default class ContentNoteList extends LightningElement 
{
    @api recordId;
    @api flexipageRegionWidth;
    @track openModal = false;
    @track messsage;    
    @track title;
    @track firstButton;
    @track secondButton;
    action;


    handleDeleteButton(event) {
        this.action = event.detail.action.name;
        
        this.firstButton = 'Cancel';
        this.secondButton = 'Delete';

        this.messsage = 'Deleting a file also removes it from any records or posts it\'s attached to.';
        this.title = 'Delete File?';
        this.openModal = true;
            
    }

    closeModal()
    {
        this.openModal = false;
    }


    firstButtonClicked()
    {
        this.openModal = false;
    }

    secondButtonClicked()
    {
        this.openModal = false;

 // add your logic here
        if (this.action === 'Delete')
        {
           
        }
        else if (this.action ==='Remove')
        {

        }
        
    }
}


We can also extend modalConfirmation LWC component. In the following example, we implmenet a modal datepick component.

6. modalDatePicker.html
The major part of is the same except a datepicker input component is to replace displaying a message.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
    <div style="height: 640px;">
            <section role="dialog" tabindex="-1" aria-labelledby="heading" aria-modal="true" aria-describedby="content" class="slds-modal slds-fade-in-open">
                <div class="slds-modal__container">
                    <header class="slds-modal__header">
                        <button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse" title="Close" onclick={closeModal}>
                            <lightning-icon icon-name="utility:close" size="medium">
                            </lightning-icon>
                            <span class="slds-assistive-text">Close</span>
                        </button>
                        <h2 class="slds-text-heading_medium slds-hyphenate">{title}</h2>
                    </header>
                    <div class="slds-modal__content slds-p-around_medium">
                        <h2><b>
                            <lightning-input class="datepicker" type="date" value={selectedDate} required></lightning-input>
                        </b></h2><br/>
                        
                    </div>
                    <footer class="slds-modal__footer">
                        <lightning-button label={firstButton} variant="neutral" onclick={firstButtonAction}></lightning-button>&nbsp;&nbsp;&nbsp;&nbsp;
                        <lightning-button label={secondButton} variant="brand" onclick={secondButtonAction}></lightning-button>
                    </footer>
                </div>
            </section>
            <div class="slds-backdrop slds-backdrop_open"></div>
        </div>
        
</template>

7. modalDatePicker.js
This component is inherited from modalConfirmation. We only need to add additional attributes (selectedDate in this case) and override the button action ( secondButtonAcion).


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import {track } from 'lwc';
import ModalConfirmation from 'c/modalConfirmation';

export default class ModalDatePicker extends ModalConfirmation {
    @track selectedDate;


    secondButtonAction(event)
    {
        if (!this.checkComponentValidity())
            return;

        // Prevents the anchor element from navigating to a URL.
        event.preventDefault();

        let inputComp =this.template.querySelector(".datepicker")

        const selectedEvent = new CustomEvent('secondbuttonclicked', { detail: inputComp.value });

        // Dispatches the event.
        this.dispatchEvent(selectedEvent);
    }

    checkComponentValidity()
    {
        let inputComp =this.template.querySelector(".datepicker"); 
        inputComp.setCustomValidity('');
        if (inputComp.value)
        {
            let inputDate = new Date(inputComp.value);
            if (inputDate < new Date())
                inputComp.setCustomValidity('Invalid Date. Date Must be a Future Date.');
        }
        return inputComp.reportValidity();

        
    }

}

8. DatePickerParentWindow.html

1
2
3
4
5
6
<template>

    <template if:true={openModalDatePicker}>
        <c-modal-date-picker title='Please select a future date' first-button="No" second-button="Yes" onfirstbuttonclicked={firstDatePickerButtonClicked} onsecondbuttonclicked={secondDatePickerButtonClicked}>
        </c-modal-date-picker>
    </template>


9.DatePickerParentWindow.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { LightningElement,wire,track } from 'lwc';

export default class DatePickerParentWindow extends LightningElement {


    @track openModalDatePicker = false;
    action;

   
    openDatePicker()
    {
        this.openModalDatePicker  = true;
    }

    firstDatePickerButtonClicked()
    {
        this.openModalDatePicker = false;
    }

    secondDatePickerButtonClicked(event)
    {
        this.openModalDatePicker = false;

 //add your logic here


    }
}


Sunday, January 12, 2020

VSCode Common Issues for Salesforce Development

1.Issue: The Salesforce CLI is not installed

After user installs Salesforce CLI, then tries to 'Create Project with Manifest', VS Code starts creating project but errs out with the following message:
The Salesforce CLI is not installed. Install it from https://developer.salesforce.com/tools/sfdxcli

Cause: Missing or incorrect Windows Environment Variable setting.

Fix: 1.1 we need to get the full path nae of installed programs: Salesfoce CLI and Microsoft VS Code.

1.2 In Windows search box, search "Edit Environment". Control panel | Path | Edit. 


It opens Path variable setting; then New | Add folders you get in last step | OK 



2. Issue: Java runtime could not be located

When starting a Salesforce project in VS Code, there is warning saying "Java runtime could not be located. Set one using the salesforcedx-vscode-apex.java.home VS Code setting. For more information, go to Set Your Java Version."

Cause: The Apex Language Server, shipped as part of the Salesforce Apex Extension for VS Code depends upon the Java Platform, Standard Edition Development Kit (JDK). It requires an installation of either JDK version 11 (Recommended) or JDK version 8. It also requires proper settings in VS Code.

Fix: 2.1 Install JDK v11 or V8. Get the full path name of installed JDK. (i.e. C:\Program Files\Java\jdk-11.0.5)
       2.2 Select File > Preferences > Settings (Windows or Linux) or Code > Preferences > Settings (macOS).
       2.3 Search for apex.
       2.4 Change the salesforcedx-vscode-apex.java.home setting to the full pathname of your Java Runtime. 

  3. Issue: unable to authorize an org behind a company firewall
There are many cases user cannot authorize an org in VS code.
The one I am addressing is this post is about company security policy does not allow using port 1717 on user's computer.

When user tries to authorize a Salesforce org with oAuth, user gets "localhost 1717 Oauth Redirect" error at the redirect step.

Cause: One of the causes is company security policy does not allow using port 1717 on your computer.

Fix:  3.1 Find a public VPN as proxy server.
        3.2 Add HTTP_Proxy and HTTPs_Proxy Windows Environment Variables
        The format is:
        https_proxy=http://username:password@proxy.example.com:port
        https_proxy=http://username:password@proxy.example.com:port