How to Add Products with Custom Button using LWC

June 23, 2023


 

In Salesforce, managing opportunities efficiently is crucial for sales teams. The ability to add products quickly and seamlessly can significantly enhance the sales process. In this blog post, we will explore how to create a custom button using Lightning Web Components (LWC) that enables users to add products directly to an opportunity, streamlining the workflow and saving valuable time.

 

After completing this unit, you will able to:

1.  What is Add Product on the Opportunity?

2.  Create a Lightning Web Component.

3.  Add the Custom Button to the Opportunity Record Page.

4.  Test the Custom Button.

 

1.  What is Add Product on the Opportunity?

Add Product on the Opportunity refers to the functionality within Salesforce that allows users to associate products with an opportunity. An opportunity represents a potential sale or deal, and adding products to an opportunity helps track the specific items or services that are part of that potential sale.

When adding products to an opportunity, users typically have access to a product catalog or list from which they can select the relevant items. The selected products are then associated with the opportunity, providing important information such as product details, quantities, pricing, and any discounts or customizations.

By adding products to an opportunity, sales teams can accurately reflect the scope of the deal, track the value of potential sales, and generate accurate quotes and forecasts. It enables a comprehensive view of the sales pipeline and assists in generating accurate reports and analytics related to product sales and revenue.

The "Add Product on the Opportunity" functionality streamlines the sales process, ensuring that all relevant product information is captured and associated with the appropriate opportunity, ultimately helping sales teams manage and close deals more effectively.

 

2.  Create a Lightning Web Component:

Before we dive into the implementation, ensure that you have a suitable development environment set up. This includes Salesforce CLI and a code editor of your choice.

To begin, first of all, we need to create an apex class named customAddProductModalClass and then new Lightning Web Component named addNewProductCustom that will represent the custom button.

Modify the customAddProductModalClass  file with the following code:

public without sharing class customAddProductModalClass {
    @AuraEnabled
    public static String findProducts(String recordId , String name , String productCode , List<String> productFamily, Integer RecordLimit) {
        Opportunity op = [SELECT Id , Pricebook2Id, Pricebook2.Name FROM Opportunity WHERE Id =:recordId];
        wrapperClass wc = new wrapperClass();
        If(op.Pricebook2Id != null){
            wc.priceBook = op.Pricebook2.Name;
        }
        String ProductQuery = 'SELECT Id,Family,Description,ProductCode,Name FROM Product2 ';
        If(productFamily != null && productFamily.size() != 0 ){
            ProductQuery += ' And Family IN '+productFamily;
        }
       
        Map<Id,Product2> mapProduct = new Map<Id,Product2>((List<Product2>)Database.query(ProductQuery));
        List<ProductWrapper> lstProduct= new List<ProductWrapper>();
        List<Id> lstProductIds = new List<Id>(mapProduct.keyset());
        String query = 'SELECT Id ,UnitPrice,Product2Id  FROM PricebookEntry WHERE Pricebook2Id = \''+ op.Pricebook2Id +'\' AND IsActive = true AND Product2Id IN :lstProductIds';
        If(name != ''){
            String val='\'%' + String.escapeSingleQuotes(name.trim()) + '%\'';
            query += ' And Name Like '+val ;
        }
        If(productCode != ''){
            query += ' And ProductCode = \''+ String.escapeSingleQuotes(productCode.trim())+'\'';
        }
        if(name != '' || productCode != '')
            query += ' limit 5';
       
        System.debug(query);
        List<PricebookEntry> lstPBE = (List<PricebookEntry>)Database.query(query);
        if(lstPBE != null && lstPBE.size() != 0){
            for(PricebookEntry pbe : lstPBE){
                ProductWrapper pw = new ProductWrapper();
                Product2 p2 = mapProduct.get(pbe.Product2Id);
                if(p2 != null){
                    pw.Id = pbe.Id;
                    pw.purl = '/lightning/r/' +pbe.Id+'/view';
                    pw.Product2Id = p2.Id;
                    pw.Name =p2.Name;
                    pw.Family = p2.Family;
                    pw.ProductCode = p2.ProductCode;
                    pw.Description= p2.Description;
                    pw.Price = pbe.UnitPrice;
                    lstProduct.add(pw);
                }
            }
        }
        wc.productList = lstProduct;
        return JSON.serialize(wc);
    }
   
    @AuraEnabled
    public static List<PicklistValue> getproductfamily(){
        String strObjectName = 'Product2';
        String strPicklistField = 'Family';
        Map<String, String> mapPickListValues = new Map<String, String>();
        Schema.SObjectType objSobjectType = Schema.getGlobalDescribe().get(strObjectName);
        Schema.DescribeSObjectResult objDescribeSobject = objSobjectType.getDescribe();
        Map<String, Schema.SObjectField> mapFields = objDescribeSobject.fields.getMap();
        List<Schema.PicklistEntry> lstPickListValues = mapFields.get(strPicklistField).getDescribe().getPickListValues();
        List<PicklistValue> pvList = new List<PicklistValue>();
        for (Schema.PicklistEntry objPickList : lstPickListValues) {
            PicklistValue pv = new PicklistValue(objPickList.getValue(), objPickList.getLabel());
            pvList.add(pv);
        }
        return pvList;
    }
   
    @AuraEnabled
    public static String saveProducts( String recordData, String recId ){
       
        List<ProductWrapper> wc  = (List<ProductWrapper>)json.deserialize(recordData, List<ProductWrapper>.class);
        List<OpportunityLineItem> lstOpp = new List<OpportunityLineItem>();
        for(ProductWrapper pw : wc){
            System.debug(' pw' + pw);
            OpportunityLineItem oli = new OpportunityLineItem();
            oli.Quantity = pw.Quantity;
            oli.UnitPrice = pw.Price;
            oli.ServiceDate = pw.PDate;
            oli.Description = pw.LineDescription;
            oli.OpportunityId = recId;//'0065j00000N2RXiAAN';
            oli.Product2Id = pw.Product2Id;
            oli.PricebookEntryId = pw.Id;//'01u5j000003Pq51AAC';
            lstOpp.add(oli);
        }
        try {
            insert lstOpp;
            return 'success';
        } catch (Exception e) {
            System.debug(e.getMessage());
            return 'error';
        }        
    }
   
    public with sharing class wrapperClass {
        public String priceBook;
        public List<ProductWrapper> productList;
    }
   
    public with sharing class ProductWrapper {
        public String Name;
        public String Id;
        public String purl;
        public String Product2Id;
        public String ProductCode;
        public Decimal Price;
        public Decimal Quantity = 0;
        public String Family;
        public Date PDate;
        public String Description;
        public String LineDescription;
    }
   
    public class PicklistValue {
        @auraenabled
        public String label {get;set;}
        @auraenabled
        public String value {get;set;}
       
        public PicklistValue(String value, String label) {
            this.value = value;
            this.label = label;
        }
    }
}

 

Navigate to the newly created AddProductButton directory and open the component files in your code editor.

Modify the addNewProductCustom.html file with the following markup:

<!-- sldsValidatorIgnore -->
<template>
  <lightning-button variant="brand"
     label="Add Product"
     title="Add Product"
     onclick={openModal}
     class="slds-m-left_x-small">
  </lightning-button>

  <template if:true={isModalOpen} style="max-width: 90rem !important;width:90% !important;">
      <section role="dialog"  aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1" class="slds-modal slds-fade-in-open">
          <div class="slds-modal__container" style="max-width: 80rem !important;width: 80% !important;">
             <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"
                        alternative-text="close"
                        variant="inverse"
                        size="small" ></lightning-icon>
                    <span class="slds-assistive-text">Close</span>
                </button>
                <template if:true={isFirstPage}>
                  <h2 id="modal-heading-02" class="slds-text-heading_medium slds-hyphenate">Add Products</h2>
                  <h3 id="modal-heading-03" >Price Book: {PriceBook}</h3>
                </template>
                <template if:false={isFirstPage}>
                  <h2 id="modal-heading-04" class="slds-text-heading_medium">Edit Selected Products</h2>
                </template>
            </header>
            <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
              <template if:true={isFirstPage}>
                <div class="slds-grid slds-gutters slds-p-around_xxx-small">
                      <div class="slds-col" style="width:100%">
                          <div class="slds-form-element" onmouseleave={toggleResult} >      
                              <div class="slds-combobox_container slds-has-selection">
                                <divclass="lookupInputContainer slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click"aria-expanded="false"aria-haspopup="listbox"role="combobox">
                                  <div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_left-right" role="none">
                                    <div class="searchBoxWrapper slds-show">
                                      <!--Lookup Input Field-->
                                      <lightning-input                  
                                          type="search"
                                          data-source="searchInputField"
                                          onclick={toggleResult}
                                          value={searchKey}
                                          variant="label-hidden"
                                          placeholder="Search Product here..."
                                          onkeyup={showFilteredProducts}
                                          class="searchBox"
                                      ></lightning-input>
                                      <span if:true={showErrorMsg} style="color:red">First, enter keywords for a product. Then add filters and click Apply.</span>
                                    </div>
                                </div>
                                  <!-- lookup search result part start-->
                                  <div style="margin-top:0px;display:none" id="listbox-id-5" class="lookupContainer slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid" role="listbox">
                                    <ul class="slds-listbox slds-listbox_vertical" role="presentation">
                                      <template for:each={lstResult} for:item="obj">
                                        <li key={obj.productCode} role="presentation" class="slds-listbox__item">
                                          <div data-recid={obj.productCode} onclick={handelSelectedRecord} class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_has-meta" role="option">
                                            <span style="pointer-events: none;" class="slds-media__figure slds-listbox__option-icon" >
                                              <span class="slds-icon_container" >
                                                  <lightning-icon icon-name="standard:product_item" size="small" alternative-text="icon" ></lightning-icon>  
                                              </span>
                                            </span>
                                            <span style="pointer-events: none;" class="slds-media__body" >
                                              <span  class="slds-listbox__option-text slds-listbox__option-text_entity">{obj.Name}</span>
                                              <span  class="slds-listbox__option-text slds-listbox__option-text_entity">{obj.ProductCode}</span>
                                            </span>
                                          </div>
                                        </li>
                                      </template>
                                      <!--ERROR msg, if there is no records..-->
                                      <template if:false={hasRecords}>
                                        <li class="slds-listbox__item" style="text-align: center; font-weight: bold;">No Records Found....</li>
                                      </template>
                                    </ul>
                                  </div>
                                </div>
                              </div>
                            </div>
                      </div>
                      <div class="slds-col">
                          <lightning-button-icon icon-name="utility:filterList"  size="medium"   onclick={datafilter} alternative-text="Filter" class="slds-m-left_xx-small slds-float_right"></lightning-button-icon>
                      </div>
                     
                  </div>
                  <div class="slds-grid slds-grid_vertical slds-m-vertical_x-small">
                    <a aria-live="polite" if:true={ShowViewAll} onclick={handleviewAll} class="slds-m-vertical_xx-small slds-col slds-p-around_xx-small" style="height: 10px !important">Back to Result</a>
                    <a aria-live="polite" if:true={ShowSelected} onclick={handleShowSelected} class="slds-m-vertical_xx-small slds-col slds-p-around_xx-small" style="height: 10px !important">Show Selected ({SelectedRecordCount})</a>
                  </div>

                  <div class="demo-only demo-only--sizing slds-grid slds-wrap slds-scrollable_y" if:true={datafilterval} style="height:21rem;">
                    <div class="slds-size_2-of-3">
                      <div class="slds-box_x-small slds-text-align_center slds-m-around_x-small">
                          <div class="slds-col slds-box">
                              <lightning-datatable  
                                data={ShowTableData}
                                key-field="ProductCode"
                                columns={cols}
                                selected-rows = {selectedProductCode}
                                onrowselection={SelectedProduct}>  
                              </lightning-datatable>  
                          </div>
                      </div>
                    </div>
                    <div class="slds-size_1-of-3">
                      <div class="slds-box_x-small slds-m-around_x-small">
                        <div class="slds-panel__header">
                          <h2 id="modal-heading-01" class="slds-panel__header-title slds-text-heading_small slds-truncate">Quick Filters</h2>
                        </div>
                        <lightning-input type="text" value={FilterForm.ProductCode} label="Product Code" name="ProductCode" onchange={handleChange}></lightning-input><br/>
                        <lightning-checkbox-group name="ProductFamily"
                        label="Product Family"
                        options={options}
                        value={FilterForm.ProductFamily}
                        onchange={handleChange}></lightning-checkbox-group>
                      </div>
                      <button class="slds-button slds-button_neutral" onclick={clearFilter}>Cancel</button>
                      <button class="slds-button slds-button_brand slds-float_right slds-m-right_medium" onclick={ApplyFilter}>Apply</button>
                    </div>
                  </div>
                  <div class="slds-grid slds-gutters slds-scrollable_y" style="height:21rem;" if:false={datafilterval}>
                      <div class="slds-col slds-box">
                          <lightning-datatable  
                            data={ShowTableData}
                            key-field="ProductCode"
                            columns={cols}
                            selected-rows = {selectedProductCode}
                            onrowselection={SelectedProduct}>  
                          </lightning-datatable>  
                      </div>
                  </div>
              </template>
              <template if:true={isSecondPage}>
                <table class="slds-table slds-table_cell-buffer slds-table_bordered slds-table_col-bordered">
                  <thead>
                    <tr id="firstRow" class="slds-line-height_reset">
                      <th class="" scope="col">
                      </th>
                      <th class="" scope="col">
                        <div class="slds-truncate" title="Item Number">Item Number</div>
                      </th>
                      <th class="" scope="col">
                        <div class="slds-truncate" title="Quantity"><span class="slds-required requiredHeader">*</span><span class="slds-truncate">Quantity</span></div>
                      </th>
                      <th class="" scope="col">
                        <div class="slds-truncate" title="Sales Price"><span class="slds-required requiredHeader">*</span><span class="slds-truncate">Sales Price</span></div>
                      </th>
                      <th class="" scope="col">
                        <div class="slds-truncate" title="Date">Date</div>
                      </th>
                      <th class="" scope="col">
                        <div class="slds-truncate" title="Line Description">Line Description</div>
                      </th>
                     
                    </tr>
                  </thead>
                  <tbody>
                    <template for:each={SelectedProductData} for:item="product" for:index="index">
                      <tr key={product.id} id={product.Id}>
                        <td data-index="index" class="slds-show">
                          <div >{index} </div>
                        </td>
                        <th data-label="Opportunity Name" scope="row">
                          <div class="slds-truncate" title="Cloudhub">
                            <a href={product.purl} tabindex="-1">{product.Name}</a>
                          </div>
                        </th>                          
                        <td>
                            <lightning-input type="text" value={product.Quantity}  placeholder="0"  data-target-id={product.Id} onchange={handleQuantityChange}></lightning-input>
                        </td>
                        <td>
                            <lightning-input type="text" value={product.Price}  placeholder="0"  data-target-id={product.Id} onchange={handleSalesPriceChange}></lightning-input>
                        </td>
                        <td>
                            <lightning-input type="date" value={product.PDate} data-target-id={product.Id} onchange={handleDateChange}></lightning-input>
                        </td>
                        <td>
                            <lightning-input type="text" value={product.LineDescription} data-target-id={product.Id} onchange={handleLineDescriptionChange}></lightning-input>
                        </td>
                        <td>
                            <lightning-button-icon icon-name="utility:delete" onclick={hadleDelete}  value={product.Id} data-target-id={product.Id} data-field="delete" alternative-text="Delete" class="slds-m-left_xx-small" title="Delete"></lightning-button-icon>
                        </td>
                      </tr>
                    </template>
                </tbody>
              </table>
              </template>
              <footer class="slds-modal__footer">
                <template if:false={isFirstPage}>
                  <button class="slds-button slds-button_brand slds-float_left" onclick={handleback} title="Next">Back</button>
                </template>
                <button class="slds-button slds-button_neutral" onclick={closeModal} title="Cancel">Cancel</button>
                <template if:true={isFirstPage}>
                  <button class="slds-button slds-button_brand" onclick={nextDetails} title="Next" disabled={DisableNext}>Next</button>
                </template>
                <template if:false={isFirstPage}>
                  <button class="slds-button slds-button_brand" onclick={saveDetails} title="Next">Save</button>
                </template>
              </footer>
            </div>
          </div>
      </section>
      <div class="slds-backdrop slds-backdrop_open"></div>
  </template>
</template>

 

In the addNewProductCustom.css file, copy and paste with the following code:

.slds-modal__container {
    max-width: 80rem !important;
    width: 80% !important;
}

 

In the addNewProductCustom.js file, add the following code:

import { LightningElement, track,wire,api } from 'lwc';
import findProducts from '@salesforce/apex/customAddProductModalClass.findProducts';
import saveProducts from '@salesforce/apex/customAddProductModalClass.saveProducts';
import getproductfamily from '@salesforce/apex/customAddProductModalClass.getproductfamily';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { RefreshEvent } from 'lightning/refresh';

const DELAY = 300;

const COLS=[  
    {label:'Product Name',fieldName:'purl', type: 'url', typeAttributes: {label: { fieldName: 'Name'}}},  
    {label:'Product Code',fieldName:'ProductCode', type:'text'},
    {label:'List Price',fieldName:'Price', type:'currency'},
    {label:'Product Description',fieldName:'Description', type:'text'},
    {label:'Product Family',fieldName:'Family', type:'text'}  
  ];
export default class AddNewProductCustom extends LightningElement {
    cols=COLS;  
    @api recordId;
    @track SelectedRecordCount = 0;
    @track isModalOpen = false;
    @track ShowSelected = true;
    @track PriceBook = '';
    @track ShowTableData = [];
    @track selectedProductCode =[];
    @track AllProductData = [];
    @track SelectedProductData = [];
    @track lstResult = [];
    @track hasRecords = true;
    @track searchKey='';
    @track isSearchLoading = false;
    @track delayTimeout;
    @track isFirstPage = true;
    @track isSecondPage = false;
    @track selectedRows = [];
    @track ShowViewAll = false;
    @track datafilterval = false;
    @track prodfamilylst = [];
    @track FilterForm = {"ProductFamily" : ""};
    @track isProductSelect = true;
    mapIdQuantity;
    mapIdSalesPrice;
    mapIdDate;
    mapIdLineDescription;
    @track showErrorMsg = false;
    @track filteredData = [];
    @track DisableNext = true;

    connectedCallback(){
        this.mapIdQuantity = new Map();
        this.mapIdSalesPrice = new Map();
        this.mapIdDate = new Map();
        this.mapIdLineDescription = new Map();
        this.getproductfamily();
        findProducts({recordId : this.recordId, name : this.searchKey , productCode : '' , productFamily : [], RecordLimit : 5 }).then(result => {
            console.log('connectedCallback = ',result);
            let dataObj = JSON.parse(result);
            console.log(dataObj);
            this.AllProductData = dataObj.productList;
            this.ShowTableData = dataObj.productList;
            this.PriceBook = dataObj.priceBook;            
        });
    }

    getproductfamily() {
        //this.isModalOpen = true;
        getproductfamily().then(result => {
            console.log('Productfamily'+result);    
            this.prodfamilylst = result;      
        });
    }

    get options() {
        return this.prodfamilylst;
    }

    handleChange(event) {
        this.FilterForm[event.target.name] = event.detail.value
    }

    openModal() {
        this.isModalOpen = true;
        findProducts({recordId : this.recordId, name : this.searchKey , productCode : '' , productFamily : [], RecordLimit : 5 }).then(result => {
            console.log(result);
            let dataObj = JSON.parse(result);
            console.log(dataObj);
            this.AllProductData = dataObj.productList;
            this.ShowTableData = dataObj.productList;
            this.PriceBook = dataObj.priceBook;            
        });
    }

    handleShowSelected(){
       
        this.ShowSelected = false;
        console.log('handleShowSelected called...');
        this.ShowTableData = this.SelectedProductData;
        this.ShowViewAll = true;
        this.RecalculateselectedProductCode();
    }

    handleviewAll(event){
        this.ShowSelected = true;
        this.ShowTableData = this.AllProductData;
        this.ShowViewAll = false;
        this.RecalculateselectedProductCode();
    }

    RecalculateselectedProductCode(){
        this.selectedProductCode = [];
        for(let i=0 ; i<this.SelectedProductData.length;i++){
            this.selectedProductCode.push(this.SelectedProductData[i].ProductCode);
        }
    }

    SelectedProduct(event) {
        console.log('SelectedProduct called..');
        if(true){
            const selRows = event.detail.selectedRows;
            if ( this.selectedRows.length < selRows.length ) {
                console.log( 'Selected' );
                for(let i = 0 ; i < selRows.length ; i++){
                    this.selectedProductCode.push(selRows[i].ProductCode);
                    //this.SelectedProductData.push(selRows[i]);
                }
            } else {
                var selectedRowsProductCode = [];
                var selProductCode = [];
                for(let i = 0;i<this.selectedRows.length;i++){
                    selectedRowsProductCode.push(this.selectedRows[i].ProductCode);
                }
                for(let i = 0 ; i < selRows.length ; i++){
                    selProductCode.push(selRows[i].ProductCode);
                }
                var deselectedRecProductCode = selectedRowsProductCode.filter(x => selProductCode.indexOf(x) === -1);
                for(let i = 0 ; i < deselectedRecProductCode.length ; i++){
                    this.selectedProductCode = this.selectedProductCode.filter(function(e) { return e !== deselectedRecProductCode[i] })
                }
            }
            this.selectedRows = selRows;
            this.selectedProductCode = [...new Set(this.selectedProductCode)];
            this.SelectedRecordCount = this.selectedProductCode.length;

            this.SelectedProductData = [];
            for(let i=0;i<this.selectedProductCode.length;i++){
                for(let j=0;j<this.AllProductData.length;j++){
                    if(this.selectedProductCode.includes(this.AllProductData[j].ProductCode)){
                        this.SelectedProductData.push(this.AllProductData[j]);
                    }
                }
            }
            this.SelectedProductData = [...new Set(this.SelectedProductData)];
            if(this.selectedProductCode.length > 0 ){
                this.DisableNext = false;
            }else{
                this.DisableNext = true;
            }
        }
        this.isProductSelect = true;
       
    }

    closeModal() {
        this.isModalOpen = false;
        this.SelectedRecordCount = 0;
       
        this.PriceBook = '';
        this.ShowTableData = [];
        this.selectedProductCode =[];
        this.AllProductData = [];
        this.SelectedProductData = [];
   
        this.lstResult = [];
        this.hasRecords = true;
        this.searchKey='';
        this.isSearchLoading = false;
        this.isFirstPage = true;
        this.isSecondPage = false;
        this.selectedRows = [];
        this.ShowViewAll = false;
        this.ShowSelected = true;
        this.showErrorMsg = false;
        this.filteredData = [];
        this.FilterForm = {"ProductFamily" : ""};
        this.datafilterval = false;
        this.DisableNext = true;
    }
    nextDetails() {
       this.isFirstPage = false;
       this.isSecondPage = true;
       this.SelectedProductData = [];
       for(let i=0;i<this.selectedProductCode.length;i++){
            for(let j=0;j<this.AllProductData.length;j++){
                if(this.selectedProductCode.includes(this.AllProductData[j].ProductCode)){
                    this.SelectedProductData.push(this.AllProductData[j]);
                }
            }
        }
        console.log('selectedProductCode = ',JSON.stringify(this.selectedProductCode));
        this.SelectedProductData = [...new Set(this.SelectedProductData)];
       clearTimeout(this.timeoutId); // no-op if invalid id
       this.timeoutId = setTimeout(this.updateIndex.bind(this), 1000);
    }

    updateIndex() {
       
    }

    datafilter(){
        if(this.datafilterval){
            this.datafilterval = false;
        }else{
            this.datafilterval = true;
        }
    }

    hadleDelete(event) {
        this.template.querySelectorAll('tr').forEach(ele => {
            console.log('ele-----------'+JSON.stringify(ele));
            console.log('event.target.value-----------'+JSON.stringify(event.target.value));
            if(ele.id.includes(event.target.value)){
                ele.classList.add('slds-hide')
            }
        });
    }

    saveDetails(){
        var deletedProducts = []
        this.template.querySelectorAll('tr').forEach(ele => {
            if(ele.classList.value.includes('slds-hide') && !ele.id.includes('firstRow')){
                var temp = ele.id.split('-');
                if(temp.length > 0){
                    deletedProducts.push(temp[0]);
                }
            }
        });
        console.log('hiddendProducts = ',deletedProducts);
        for (var i = 0; i < this.SelectedProductData.length; i++){            
            var obj = this.SelectedProductData[i];
            for (var key in obj){
              var value = obj[key];
                if(key === 'Id'){
                    if(this.mapIdQuantity.get(value) != undefined){
                        obj.Quantity = this.mapIdQuantity.get(value);
                    }
                    if(this.mapIdSalesPrice.get(value) != undefined){
                        obj.Price = this.mapIdSalesPrice.get(value);
                    }
                    if(this.mapIdDate.get(value) != undefined){
                        obj.PDate = this.mapIdDate.get(value);
                    }
                    if(this.mapIdLineDescription.get(value) != undefined){
                        obj.LineDescription = this.mapIdLineDescription.get(value);
                    }
                }        
            }
            this.SelectedProductData[i] = obj;
        }
        var DataToSave = this.SelectedProductData;
        this.SelectedProductData = [];
        var isValidate = true;
        for (var i = 0; i < DataToSave.length; i++){
            if(!deletedProducts.includes(DataToSave[i]["Id"])){
                this.SelectedProductData.push(DataToSave[i]);
            }
        }

        for (var i = 0; i < this.SelectedProductData.length; i++){
            if(this.SelectedProductData[i]["Quantity"] == 0 || this.SelectedProductData[i]["Quantity"] == undefined){
                isValidate = false;
                break;
            }
        }
        if(isValidate){
            this.isFirstPage = false;
            console.log(' SelectedProductData ' + JSON.stringify(this.SelectedProductData));
            let str = JSON.stringify(this.SelectedProductData);        
            saveProducts({recordData : str, recId : this.recordId }).then(result => {            
                this.selectedRecord = [];
                this.dispatchEvent(new ShowToastEvent({
                    title: 'Success',
                    message: 'Product Added Successfully',
                    variant: 'success',
                }));
                this.dispatchEvent(new RefreshEvent());
                this.closeModal();
           
            })
            .catch(error => {            
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Error Product Adding',
                        message: error.body.message,
                        variant: 'error',
                    }),
                );  
                this.updateRecordView();
                this.closeModal();            
            });
        }else{
            this.dispatchEvent(new ShowToastEvent({
                title: 'Error',
                message: 'Quantity should be non-Zero',
                variant: 'error',
            }));
        }

    }

    handleback(){
        this.ShowTableData = this.AllProductData;
        this.ShowSelected = true;
        this.isFirstPage = true;
       this.isSecondPage = false;
       mapIdQuantity = '';
       mapIdSalesPrice = '';
       mapIdDate = '';
       mapIdLineDescription = '';

    }

    fillselectedRows(){
        this.selectedRows = []
        for(let i = 0 ; i < this.ShowTableData.length ; i++){
            if(this.selectedProductCode.includes(this.ShowTableData[i].ProductCode)){
                console.log('pushed');
                this.selectedRows.push(this.ShowTableData[i]);
            }
        }
    }
    showFilteredProducts(event){
        console.log('event.keyCode = ',event.keyCode);
        if(event.keyCode == 13){
            this.isFirstPage  = false;
            this.showErrorMsg = false;
            findProducts({recordId : this.recordId, name : this.searchKey , productCode : '' , productFamily : [], RecordLimit : 1000 }).then(result => {
                let dataObj = JSON.parse(result);
                console.log(dataObj);
                this.ShowTableData = dataObj.productList;
                this.filteredData = dataObj.productList;
                this.fillselectedRows();
                this.isFirstPage  = true;
                this.ShowViewAll = true;
                this.ShowSelected = true;
                /*const searchBoxWrapper = this.template.querySelector('.lookupContainer');
                searchBoxWrapper.classList.remove('slds-show');
                searchBoxWrapper.classList.add('slds-hide');*/
            });
        }else{
            this.handleKeyChange(event);
            const searchBoxWrapper = this.template.querySelector('.lookupContainer');
            searchBoxWrapper.classList.add('slds-show');
            searchBoxWrapper.classList.remove('slds-hide');
        }
    }
       
    handleKeyChange(event) {
       
        this.isSearchLoading = true;
        this.searchKey = event.target.value;
        findProducts({recordId : this.recordId, name : this.searchKey , productCode : '' , productFamily : [], RecordLimit : 5 }).then(result => {
            let dataObj = JSON.parse(result);
            console.log(dataObj);
            this.hasRecords = dataObj.productList.length == 0 ? false : true;
            this.lstResult = dataObj.productList;
            console.log(this.lstResult );
        });
    }


    toggleResult(event){
        console.log('toggleResult called...');
        const lookupInputContainer = this.template.querySelector('.lookupInputContainer');
        const clsList = lookupInputContainer.classList;
        const whichEvent = event.target.getAttribute('data-source');
        switch(whichEvent) {
            case 'searchInputField':
                clsList.add('slds-is-open');
               break;
            case 'lookupContainer':
                clsList.remove('slds-is-open');    
            break;                    
           }
    }

    handelSelectedRecord(event){  
        console.log(' event.target.dataset '+ JSON.stringify(event.target.dataset));
        console.log(' event.target '+ JSON.stringify(event.target));

        var objId = event.target.dataset.recid;
        console.log(' objId ' + objId);
        const searchBoxWrapper = this.template.querySelector('.lookupContainer');
        searchBoxWrapper.classList.remove('slds-show');
        searchBoxWrapper.classList.add('slds-hide');
        this.selectedRecord = this.lstResult.find(data => data.productCode === objId);
        this.selectedProductCode.push(this.selectedRecord.ProductCode);
        this.SelectedRecordCount += 1;
        this.ShowTableData.push(this.selectedRecord);

        this.handleShowSelected();
    }

    handleQuantityChange(event)
    {
        var selectedRow = event.currentTarget;
        var key = selectedRow.dataset.targetId;
        console.log( ' key ' + key + ' event.target.value ' + event.target.value);
        this.mapIdQuantity.set(key, event.target.value);
    }

    handleSalesPriceChange(event)
    {

        var selectedRow = event.currentTarget;
        var key = selectedRow.dataset.targetId;
        this.mapIdSalesPrice.set(key, event.target.value);
    }

    handleDateChange(event)
    {
        var selectedRow = event.currentTarget;
        var key = selectedRow.dataset.targetId;
        this.mapIdDate.set(key, event.target.value);
    }

    handleLineDescriptionChange(event)
    {
        var selectedRow = event.currentTarget;
        var key = selectedRow.dataset.targetId;
        this.mapIdLineDescription.set(key, event.target.value);
    }

    ApplyFilter(){
        const searchBox = this.template.querySelector('.searchBox');
        if(searchBox.value == '' || searchBox.value == undefined){
            this.showErrorMsg = true;
        }else{
            this.isFirstPage  = false;
            findProducts({recordId : this.recordId, name : this.searchKey , productCode : '' , productFamily : [], RecordLimit : 1000 }).then(result => {
                let dataObj = JSON.parse(result);
                console.log(dataObj);
                this.ShowTableData = dataObj.productList;
                this.filteredData = dataObj.productList;
                this.fillselectedRows();
                this.isFirstPage  = true;
                this.ShowViewAll = true;
                this.ShowSelected = true;

                if(this.FilterForm["ProductCode"] != undefined){
                    var filteredProductData = [];
                    for(let i = 0 ; i < this.filteredData.length ; i++){
                        if( this.filteredData[i].ProductCode.toLowerCase().includes(this.FilterForm["ProductCode"].toLowerCase())){
                            if( this.FilterForm["ProductFamily"] != undefined && this.FilterForm["ProductFamily"].length != 0){
                                for(let j = 0 ; j < this.FilterForm["ProductFamily"].length ; j++){
                                    console.log('this.ShowTableData[i].Family = ',this.filteredData[i].Family);
                                    if(this.FilterForm["ProductFamily"][j] == this.filteredData[i].Family){
                                        filteredProductData.push(this.filteredData[i]);
                                        break;
                                    }
                                }
                            }else{
                                filteredProductData.push(this.filteredData[i]);
                            }
                           
                        }
                    }
                    this.showErrorMsg = false;
                    this.ShowTableData = filteredProductData;
                    this.isProductSelect = false;
                    this.fillselectedRows();
                    this.RecalculateselectedProductCode();
                    console.log('filteredProductData = ',filteredProductData);
                }else if(this.FilterForm["ProductFamily"] != undefined && this.FilterForm["ProductFamily"].length != 0){
                    var filteredProductData = [];
                    for(let i = 0 ; i < this.filteredData.length ; i++){
                        for(let j = 0 ; j < this.FilterForm["ProductFamily"].length ; j++){
                            if(this.FilterForm["ProductFamily"][j] == this.filteredData[i].Family){
                                filteredProductData.push(this.filteredData[i]);
                                break;
                            }
                        }
                    }
                    this.showErrorMsg = false;
                    this.ShowTableData = filteredProductData;
                    this.isProductSelect = false;
                    this.fillselectedRows();
                    this.RecalculateselectedProductCode();
                    console.log('filteredProductData = ',filteredProductData);
                }
               
            });
        }

    }

    clearFilter(){
        this.FilterForm = {"ProductFamily" : ""};
    }

}

 

In the addNewProductCustom.js-meta.xml file, add the following code:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordAction</target>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>
 
In this code, we import two things: One is the ShowToastEvent from the lightning/platformShowToastEvent module to handle the success message and the error message on the UI.  The other is all the methods from the customAddProductModalClass to access the methods in the js file.

 

3. Add the Custom Button to the Opportunity Record Page:

Now that the custom button component is ready, let's add it to the Opportunity record page.

1.  In your Salesforce org, go to the Opportunity record detail page you want to modify.

2.  Click on the Edit Page button from the gear icon on the page.

 

 

3.  Find the addNewProductCustom Lightning Web Component in the search box on the Components tab from the left side of your page. you will see the Lightning Web Component which we created in the above steps.

 

 

4.  Drag the component on the Related panel.

 

 

5.  Save the changes and Activate the page if asked.

 

4.  Test the Custom Button:

To ensure that everything is working correctly, navigate to an Opportunity record where you've added the custom button. You should see the "Add Product" button in the specified section of the page layout.

When you click the button, it will open the popups to show all the products. From there, you can add products directly to the opportunity, streamlining the process and improving productivity.

 

In this blog post, we walked through the process of creating a custom button using Lightning Web Components (LWC) to simplify the addition of products to opportunities in Salesforce.

Try implementing this custom button in your Salesforce org and witness the benefits firsthand!

I hope this blog helped you!