Friday, January 6, 2023

Customise native Salesforce components using CSS

 

Customise native Salesforce components using CSS

There are many instances where clients demand their own look and feel instead of native Salesforce UI. This varies from small changes to complete page changes. I have experienced such instances several times in my career and expect you to have encountered the same at some point in time. So, today I am going to cover a few methods of CSS tweaks that we can do on native instances of Salesforce UI.

 

 

 

Method 1

Use lightning aura:html component with style.

<aura:html tag="style">

/* Your css */

</aura:html>

 

To understand how we can utilize the power of this component, let's consider a use case where the Client wants to keep all the buttons on the account page should have background colour as orange.

Step 1) Grab the tag specification, here we will going to use the CSS class of the tag.

Step 2) Create an aura component and add style as mentioned in the image.

Also, make sure you have added the right interface under the component implements attribute.

 

Step 3) Visit the account detail page and then go to setup->edit page

Drag and drop your custom component over the page.

Activate and then save the page.

 

Step 4) Go back to the page and there you will see the changes.

Method 2

If you want to keep your CSS organized and minify you can use this second approach. In this method, we build one CSS file and attach it to a static resource.

 

Step 1) Create a new CSS file and add your CSS changes there.

Step 2) Add your css file to static resource

Step 3) Add style to your component

<ltng:require styles="{!$Resource.AccountCSS}" />

Step 4) Add component to your account detail page as mentioned in Method 1.

Connect Azure DevOps Repo to SourceTree

 

Connect Azure DevOps Repo to SourceTree

In this article, we will be going to learn how to connect Azure DevOps Repos with SourceTree.

Sourcetree is a tool that simplifies connecting your Git repositories. Using SourceTree you can visualize and manage your repositories through GUI.

 

Generate git credentials

 

For connecting SourceTree, we need credentials of Azure DevOps repo. Follow the steps to generate credentials:

Step 1) Go to your branch

 

Visit your branc

 

Step 2) Click the clone button

 

image

 

Step 3) Click Generate Git Credentials (we are using https instead of ssh)

image

 

Step 4) Copy your username and password and save them somewhere because you won't get it again.

image

 

 

Add Azure DevOps git account to SourceTree

 

Step 1) Open SourceTree and click on Account.

 

image

 

Step 2) Click Add to attach the new account and then add the following details

 

HostChoose Azure DevOps
urlhttps://[[yourdomain]].visualstudio.com/ Where [[yourdomain]] is your DevOps domain
Auth TypeKeep default value
UsernameYour username
PasswordYour password
Protocolhttps
image

 

 

Clone repo to SourceTree

We have multiple ways to clone the repo. The first method is to clone by adding URL manually. In this method, we clone the repo by providing a repo URL.

Step 1) Open SourceTree and click on new and then select "Clone from URL"

 

image

Step 2) Enter the source URL which we get from DevOps

Note: If your cloned URL is in the format of https://<<yourdomain>>@dev.azure.com then use https://<<yourusername>>@dev.azure.com.

 

image
image

 

It will ask you to provide your username and password to authenticate.

Step 3)  After authentication, click clone.

 

 

image

 

The second method to clone the repo is as follows:

Step 1) Open SourceTree and click on Remote

 

image

 

Step 2) Click clone

Your cloned repo is ready to create a branch,  do push, pull and commit.

Tuesday, March 13, 2018

lightning:outputField rendering issue when using with lightning:recordViewForm



Background:

lightning:outputField is the recommended tag for lightning:recordViewForm to display field value of record.

<lightning:recordViewForm recordId="001XXXXXXXXXXXXXXX" objectApiName="Contact">
        <div class="slds-box slds-theme_default">
            <lightning:outputField fieldName="Name" />
            <lightning:outputField fieldName="Phone"/>
            <lightning:outputField fieldName="Email" />
            <lightning:outputField fieldName="Birthdate" />
            <lightning:outputField fieldName="LeadSource" />
        </div>
</lightning:recordViewForm>


Problem: Unfortunately it gives error if we populate data conditionally using <aura:if tag.


 <aura:if isTrue="{!v.editMode}">
            <lightning:recordEditForm recordId="{!v.sourceRecordFields.AccountId}"
                                      onsuccess="{!c.handleAccountSaved}"
                                      objectApiName="Account">
             
                <lightning:messages />
             
                <lightning:inputField fieldName="Name" />
                <lightning:inputField fieldName="Phone" />
                <lightning:inputField fieldName="Rating" />
                <lightning:inputField fieldName="Description" />
                <lightning:button variant="brand" type="submit" name="save" label="Save" class="slds-m-top_medium"/>

            </lightning:recordEditForm>
<aura:set attribute="else">
                <lightning:recordViewForm recordId="{!v.sourceRecordFields.AccountId}"
                                          objectApiName="Account">
                 
                    <lightning:outputField fieldName="Name" />
                    <lightning:outputField fieldName="Phone" />
                    <lightning:outputField fieldName="Rating" />
                    <lightning:outputField fieldName="Description" />
                    <lightning:button variant="brand" onclick="{!c.editAccount}" name="edit" label="Edit" class="slds-m-top_medium"/>
                </lightning:recordViewForm>
             </aura:set>
</aura:if>


So, as soon as {!v. editMode} returns false, we get error for lightning:outputField.

Solution:  Lets talk about solution. Here javascript and css plays good role to show and hide conditionally.

{Component}.cmp:

<div id="editableDiv" style="display:none;">
            <lightning:recordEditForm recordId="{!v.sourceRecordFields.AccountId}"
                                      onsuccess="{!c.handleAccountSaved}"
                                      objectApiName="Account">
             
                <lightning:messages />
             
                <lightning:inputField fieldName="Name" />
                <lightning:inputField fieldName="Phone" />
                <lightning:inputField fieldName="Rating" />
                <lightning:inputField fieldName="Description" />
                <lightning:button variant="brand" type="submit" name="save" label="Save" class="slds-m-top_medium"/>

            </lightning:recordEditForm>
         </div>
         <div id="viewableDiv" style="display:block">
             <lightning:recordViewForm recordId="{!v.sourceRecordFields.AccountId}"
                                       objectApiName="Account">
               
                 <lightning:outputField fieldName="Name" />
                 <lightning:outputField fieldName="Phone" />
                 <lightning:outputField fieldName="Rating" />
                 <lightning:outputField fieldName="Description" />
                 <lightning:button variant="brand" onclick="{!c.editAccount}" name="edit" label="Edit" class="slds-m-top_medium"/>
             </lightning:recordViewForm>
         </div>

{ComponentController}.js:

({
    editAccount: function(cmp, event, helper) {
        helper.showEditableDiv(cmp);
    },
    handleAccountSaved: function(cmp, event, helper) {
        helper.hideEditableDiv(cmp);
    }
 
})


{ComponentHelper}.js:

({
showEditableDiv : function(cmp) {
var elementEdit = document.getElementById('editableDiv');
        elementEdit.style.display = 'block';
     
        var elementView = document.getElementById('viewableDiv');
        elementView.style.display = 'none';
},
    hideEditableDiv : function(cmp) {
var elementEdit= document.getElementById('editableDiv');
        elementEdit.style.display = 'none';
     
        var elementView = document.getElementById('viewableDiv');
        elementView.style.display = 'block';
}
 
})


Note: This is temporary solution and can be use until Salesforce fix it.

Happy coding!!

Thanks
Gulshan Raj
Sr. Developer






Tuesday, May 2, 2017

Sync quote line item custom fields with opportunity line item.



Background:

Quotes in Salesforce represent the proposed prices of your company’s products and services. You create a quote from an opportunity and its products. Each opportunity can have multiple associated quotes, and any one of them can be synced with the opportunity. When a quote and an opportunity are synced, any change to line items in the quote syncs with products on the opportunity, and vice versa.

But this sync is happening for all standard fields not for custom fields.

Lets see things in real:

I have opportunity record with no product and quotes initially:



Add quote under this opportunity record:

Add some products under this quote (i.e Quote Line Items):


Now when we click start sync button on quote, it populate all products under this quote to the associated opportunity (i.e Opportunity Line Item).


So far so good.
If you change any value on quote line item, it will immediately reflect value on respective opportunity line item. 

Problem: Custom fields of opportunity line item and quote line item does not sync. Which means if any value of custom field for quote line item record changes it will not reflect under opportunity line item.

There is no point and click approach where we can map opportunity line items field with quote line item field.

Solution:  

Step 1) Create custom formula field on Quote line item (I call it "Opportunity Line Item ID" )

This will give you synced opportunity line item id.

Note: This field is hidden and you will not find it in formula field picker. You have to use as it is OpportunityLineItem.Id. If you click on check syntax, this will give you success message as shown above.

Step 2) Handler class for  QuoteLineItem trigger

public class QuoteLineItemTriggerHandler
{
    
    public static void syncQuotes(List<QuoteLineItem> newLineItems)
    {
        // get quote ids we need to query for
        Set<Id> quoteIds = new Set<Id>();
        for (QuoteLineItem qli : newLineItems)
        {
            if (qli.QuoteId != null)
            {
                quoteIds.add(qli.QuoteId);
            }
        }

        // Linking quote line item with Opportunity Line Items
        Map<ID,ID> mapQuoteLineItemSortOrder= returnDefaultLinking(quoteIds);
        
        //Fetch opportunity line item for sync
        Map<ID,OpportunityLineItem> mapOppLineItems=new Map<ID,OpportunityLineItem>();
        for(OpportunityLineItem oli:[select id, Renewal__c from OpportunityLineItem where Opportunity.SyncedQuoteId in :quoteIds])
        {
            mapOppLineItems.put(oli.id,oli);
        }
        
        List<OpportunityLineItem> lstOppotunityToUpdate = new List<OpportunityLineItem>();
        for (QuoteLineItem qli : newLineItems) {
            OpportunityLineItem oli = mapOppLineItems.get(mapQuoteLineItemSortOrder.get(qli.Id));
            if (oli != null ) {
                oli.Renewal__c=qli.Renewal__c;
                //update more fields....
                
                lstOppotunityToUpdate.add(oli);
            }
        }
        update lstOppotunityToUpdate;
    }
    private static Map<ID,ID> returnDefaultLinking(Set<ID> poIds)
    {
        Map<ID,ID> mapSortOrder= new Map<ID,ID>();
        String query='select id, name,(select id, Opportunity_Line_Item_ID__c from QuoteLineItems  ) from Quote where id in :poIds';
        List<Quote> lstQuotesWithLineItems=Database.query(query);
        for(Quote q: lstQuotesWithLineItems)
        {
            if(q.QuoteLineItems !=null)
            {
                for(QuoteLineitem qli : q.QuoteLineItems)
                {
                    if(qli.Opportunity_Line_Item_ID__c!=null)
                    {
                        //map quote line item id with respective opportunity line item id
                         mapSortOrder.put(qli.Id,ID.valueOF(qli.Opportunity_Line_Item_ID__c));
                    } 
                }
            }
        }
        return mapSortOrder;
    }
}

Step 3)  QuoteLineItem Trigger 

trigger QuoteLineItemTrigger on QuoteLineItem ( after update) {
    QuoteLineItemTriggerHandler.syncQuotes(Trigger.new);
}

Step 4) Handler for OpportunityLineItem Trigger

public class OpportunityLineItemTriggerHandler{
    public static Boolean isTriggerFire = true;
    public static void sync(Set<Id> oliIds ){
        List<OpportunityLineItem> lstQLIUpdate = new List<OpportunityLineItem>();
        for(QuoteLineItem qli: [Select id, Renewal__c, Opportunity_Line_Item_ID__c from QuoteLineItem WHERE pp_dev3__Opportunity_Line_Item_ID__c= :oliIds]){
            lstQLIUpdate.add(new OpportunityLineItem(Id=qli.Opportunity_Line_Item_ID__c, Renewal__c=qli.Renewal__c));
        }
        if(!lstQLIUpdate.isEmpty()){
            isTriggerFire = false;
            update lstQLIUpdate;
            isTriggerFire = true;
        }
    }
}

Step 5) OpportunityLineItem Trigger

trigger OpportunityLineItemTrigger on OpportunityLineItem (after insert) {
    if(Trigger.isInsert && Trigger.isAfter && OpportunityLineItemTriggerHandler.isTriggerFire){
        Set<Id> qliIds = Trigger.newMap.keyset();
        OpportunityLineItemTriggerHandler.sync(qliIds);
    }
}

Custom field data is also get sync now!!