KanBan Gif

Welcome to the technical documentation for the Drag-and-Drop Lightning Web Component (LWC). This guide will walk you through the implementation and usage of this component, which provides a streamlined interface for managing opportunities using a Kanban-style view, with our own custom customization

Installation and Setup

  • Salesforce Environment Setup: Before you begin, make sure you have a Salesforce environment set up for Lightning Component development.
  • Deploy the LWC: To use the Drag-and-Drop LWC in your org, follow these steps:
    • Retrieve the source code of the component.
    • Deploy the component to your Salesforce org using Salesforce CLI or another appropriate deployment method.

Component Architecture

The Drag-and-Drop LWC is structured as follows:

  • dragAndDropLwc Folder:
    • dragAndDropLwc.html: This file contains the HTML template for the component’s user interface,and child componet of drag-and-drop-list
    • dragAndDropLwc.js: The JavaScript file holds the component’s logic, including data fetching, drag-and-drop handling, and stage updates.
    • dragAndDropLwc.css: CSS styling to enhance the component’s visual appearance.
  • dragAndDropList Folder:
    • dragAndDropList.html: This file contains the HTML template for the component’s user interface.
    • dragAndDropList.js: The JavaScript file holds the component’s logic.
  • dragAndDropCard Folder:
    • dragAndDropCard.html: This file contains the HTML template for the component’s user interface.
    • dragAndDropCard.js: The JavaScript file holds the component’s logic,

The primary purpose is to create an interactive interface where stages are displayed as tab-like elements, each showing the stage’s name and the count of records associated with it. Users can engage with these stages using drag-and-drop functionality. The component’s structure includes an iteration loop to dynamically generate these elements for each stage. Additionally, the component utilizes a nested child component, c-drag-and-drop-list, to display and manage the records associated with each stage. The main goal is to provide an intuitive way for users to view, interact with, and rearrange records across different stages through a visually engaging and user-friendly interface.

 <template>
    <div class="card_Design">
        <template for:each={pickVals} for:item="item">
            <div class="slds-tabs--path"  role="application" key={item}  style = {equalwidthHorizontalAndVertical}>
                <ul class="slds-tabs--path__nav" role="tablist">
                <li class="slds-tabs--path__item slds-is-incomplete  slds-path__item slds-is-current slds-is-active "  role="presentation">
                    <a class="slds-tabs--path__link slds-button_brand " tabindex="-1" role="tab" >
                        <span class="slds-tabs--path__title" >
                                <div class="stage-info">
                                    <span class="stage-name">{item.stage}</span>
                                    <span class="record-count">({item.noRecords})</span>
                                </div>
                        </span>
                    </a>
                </li>
            </ul>
            <c-drag-and-drop-list records={records} stage={item.stage}
            onlistitemdrag={handleListItemDrag}
            onitemdrop={handleItemDrop}
            ></c-drag-and-drop-list>
            </div>
            
        </template>
    </div>   
    
</template> 

dragAndDropLwc.js

import { LightningElement, wire } from 'lwc';
import {getObjectInfo } from 'lightning/uiObjectInfoApi';
import OPPORTUNITY_OBJECT from '@salesforce/schema/Opportunity'
import STAGE_FIELD from '@salesforce/schema/Opportunity.StageName'
import ID_FIELD from '@salesforce/schema/Opportunity.Id';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getTopOpportunities from '@salesforce/apex/OpportunityKanbanController.getTopOpportunities';
import updateOpprtuity from '@salesforce/apex/OpportunityKanbanController.updateOpprtuity';
export default class DragAndDropLwc extends LightningElement {
    records;
    stageList;
    pickVals;
    stageMap;
    recordId;

    @wire(getObjectInfo, {objectApiName:OPPORTUNITY_OBJECT})
    objectInfo

       connectedCallback() {
           getTopOpportunities()
            .then(result => {
                console.log("Inside ConnectedCallback");
                this.records = result.opportunities;
                this.pickVals = result.pickVals;
            })
            .catch(error => {
                console.log('getTopOpportunities error==>',JSON.stringify(error));
                
            });
       }

    
    get equalwidthHorizontalAndVertical(){
        let len = this.pickVals.length +1
        return `width: calc(100vw/ ${len})`
    }

    handleListItemDrag(event){
        this.recordId = event.detail
    }

    handleItemDrop(event){
        let stage = event.detail
        this.updateHandler(stage)
    }
    updateHandler(stage){
        const fields = {};
        fields[ID_FIELD.fieldApiName] = this.recordId;
        fields[STAGE_FIELD.fieldApiName] = stage;
        const recordInput ={fields}
        
        updateOpprtuity({ recordId: this.recordId, stageName: stage })
            .then(result => {
                console.log("Updated Successfully");
                this.showToast();
                this.records = result.opportunities;
                this.pickVals = result.pickVals;
                
            })
            .catch(error => {
                
            });
    }

    showToast(){
        this.dispatchEvent(
            new ShowToastEvent({
                title:'Success',
                message:'Stage updated Successfully',
                variant:'success'
            })
        )
    }

    
}

dragAndDropLwc.css

.card_Design{
    padding: 0.5rem;
    display: flex;
    justify-content: space-around;
    background: #fff;
}
.column_heading{
    padding: 0.5rem 0.25rem;
    font-size: 16px;
    background: #005fb2;
    color: #fff;
    margin-bottom: 1rem;
}
.stageContainer{
    float:left;
    border: 2px solid #d8dde6;
    margin: 0.05rem;
    border-radius: .25rem;
}
.slds-text-heading--small{
    font-size: 8 px;
    text-align: left;
    align-items:left ;
}
.slds-tabs--path__title{
    text-align: left;
}

.stage-info {
        display: flex;
        flex-direction: column;
        align-items: left;
        text-align: center;
    }

.stage-name {
       font-size: 13 px;
        line-height: 15px;
    }

.record-count {
        font-size: 13 px;
        line-height: 16px;
        align-items: center;
    }

OpportunityKanbanController.apxc

public with sharing class OpportunityKanbanController {
   
    @AuraEnabled(cacheable=true)
    public static OpportunityKanbanController.wrapper  getTopOpportunities(){
        OpportunityKanbanController.wrapper wrapperResponse = new OpportunityKanbanController.wrapper();
          List<Opportunity>  opps= [
                SELECT Id, Name,Priority__c,CloseDate, Account.name, StageName FROM Opportunity 
                 ORDER BY LastModifiedDate DESC NULLS LAST ];    
            List<Opportunity> oppList = new List<Opportunity>();
                 
        for(Opportunity op : opps){
            if( op.Priority__c !=null && op.Priority__c=='High' ){
                oppList.add(op);
            }
        }
   
         for(Opportunity op : opps){
            if( op.Priority__c == null ){
                oppList.add(op);
            }
        }
        wrapperResponse.opportunities = oppList;

            String objectName = 'Opportunity';
            String fieldName ='StageName';
            Map<String,Integer> stageNameMap = new Map<String,Integer>();
            Schema.SObjectType s = Schema.getGlobalDescribe().get(objectName) ;
            Schema.DescribeSObjectResult r = s.getDescribe() ;
            Map<String,Schema.SObjectField> fields = r.fields.getMap() ;
            Schema.DescribeFieldResult fieldResult = fields.get(fieldName).getDescribe();
            List<Schema.PicklistEntry> ple = fieldResult.getPicklistValues();
            for( Schema.PicklistEntry pickListVal : ple){
                if(!stageNameMap.containsKey(pickListVal.getValue())){
                    stageNameMap.put(pickListVal.getValue(), 0);
                }
            }
            for(Opportunity opp: oppList){
                if(stageNameMap.containsKey(opp.StageName)){
                    Integer count = 0;
                    count = stageNameMap.get(opp.StageName);
                    stageNameMap.put(opp.StageName, ++count);
                }
            }
            List<OpportunityKanbanController.stageObject> pickValList = new List<OpportunityKanbanController.stageObject>();
            for(String stage: stageNameMap.keySet()){
                OpportunityKanbanController.stageObject pickVal = new OpportunityKanbanController.stageObject();
                pickVal.stage = stage;
                pickVal.noRecords = stageNameMap.get(stage);
                pickValList.add(pickVal);
            }
            wrapperResponse.pickVals = pickValList;


        return wrapperResponse;
    }
    @AuraEnabled
    public static OpportunityKanbanController.wrapper updateOpprtuity(String recordId, String stageName){
        if(recordId != null && stageName != null){
            Update new Opportunity(id = recordId , StageName = stageName);
        }
        return OpportunityKanbanController.getTopOpportunities();
    }
    public class wrapper{
        @AuraEnabled
        public List<Opportunity> opportunities = new List<Opportunity>(); 
        @AuraEnabled
        public List<OpportunityKanbanController.stageObject> pickVals = new List<OpportunityKanbanController.stageObject>();

    }
    public class stageObject{
        @AuraEnabled
        public String stage = '';
        @AuraEnabled
        public Integer noRecords = 0;
    }
}

dragAndDropList.html

<template>
    
     <ul class="slds-has-dividers_around-space dropZone" 
    ondrop={handleDrop}
    ondragover={handleDragOver}
    style="height:70vh; overflow-y:auto;">
        <template for:each={records} for:item="recordItem">
             
            <c-drag-and-drop-card stage={stage} record={recordItem} 
            key={recordItem.Id} onitemdrag={handleItemDrag}>
            
            </c-drag-and-drop-card>
             
        </template>
    </ul> 


</template>

dragAndDropList.js

import { LightningElement, api } from 'lwc';

export default class DragAndDropList extends LightningElement {
    @api records
    @api stage
    handleItemDrag(evt){
        const event = new CustomEvent('listitemdrag', {
            detail: evt.detail
        })
        this.dispatchEvent(event)
    }
    handleDragOver(evt){
        evt.preventDefault()
    }
    handleDrop(evt){
        const event = new CustomEvent('itemdrop', {
            detail: this.stage
        })
        this.dispatchEvent(event)
    }
}

dragAndDropCard.html

<template>
<template if:true={isSameStage} for:index="index">
   <li class="slds-item slds-var-m-around_small" draggable="true" ondragstart={itemDragStart} >
       <article class="slds-tile slds-tile_board">
           <h3 class="slds-truncate" title={record.Name}>
               <a href="#" data-id={record.Id} onclick={navigateOppHandler}>
                   <span class="slds-truncate" data-id={record.Id}>
                   {index}.{record.Name}
                   </span>
               </a>
           </h3>
           <div class="slds-tile__detail slds-text-body_small">
               <p class="slds-text-heading_small">Priority:{record.Priority__c}</p>
               <p class="slds-truncate" title={record.AccountName}>
                    <a href="#" data-id={record.AccountId} onclick={navigateAccHandler}>
                    <span class="slds-truncate" data-id={record.AccountId}>
                        {record.AccountId}
                    </span>
                </a>
               </p>

               <p class="slds-truncate" title={record.StageName}>{record.StageName}</p>
           </div>
       </article>
   </li>
</template>
</template>
.slds-item {
    border: 1px solid #d8dde6;
    border-radius: 0.25rem;
    background-clip: padding-box;
    padding: 0.45rem;
    margin: 0.25rem;
   
}
import { LightningElement, api } from 'lwc';
import { NavigationMixin } from 'lightning/navigation'
export default class DragAndDropCard extends NavigationMixin(LightningElement) {
    @api stage
    @api record

    get isSameStage(){
        return this.stage === this.record.StageName
    }
    navigateOppHandler(event){
        event.preventDefault()
        this.navigateHandler(event.target.dataset.id, 'Opportunity')
    }
    navigateAccHandler(event){
        event.preventDefault()
        this.navigateHandler(event.target.dataset.id, 'Account')
    }
    navigateHandler(Id, apiName) {
        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: {
                recordId: Id,
                objectApiName: apiName,
                actionName: 'view',
            },
        });
    }
    itemDragStart(){
        const event = new CustomEvent('itemdrag', {
            detail: this.record.Id
        })
        this.dispatchEvent(event)
    }
}

Code Explanation

Let’s dive into the code that powers the Drag-and-Drop LWC. JavaScript

Fetching Opportunities and Stages

The connectedCallback method is called when the component is connected to the DOM. Inside this method, the getTopOpportunities Apex method is used to fetch a list of opportunities and their associated stages. The fetched data is stored in the records and pickVals properties.

connectedCallback() {

    getTopOpportunities()

        .then(result => {

            this.records = result.opportunities;

            this.pickVals = result.pickVals;

        })

        .catch(error => {

            console.error(‘Error fetching opportunities:’, error);

        });

}

Rendering Kanban-Style Stages

The for:each loop in the HTML template iterates over each stage in the pickVals array and renders a tab for each stage. The equalwidthHorizontalAndVertical getter calculates the width of each tab based on the number of stages.

<template for:each={pickVals} for:item=”item”>

    <div class=”slds-tabs–path” role=”application” key={item} style={equalwidthHorizontalAndVertical}>

        <!– Render stage tabs here –>

    </div>

</template>

Drag-and-Drop Functionality

The handleListItemDrag method is triggered when an opportunity is dragged within the list. It captures the ID of the dragged opportunity.

handleListItemDrag(event) {

    this.recordId = event.detail;

}

Updating Opportunity Stages

The handleItemDrop method is called when an opportunity is dropped onto a new stage. This method triggers the updateHandler to update the opportunity’s stage.

handleItemDrop(event) {

    const stage = event.detail;

    this.updateHandler(stage);

}

Class: OpportunityKanbanController

Purpose

This class provides backend functionality for retrieving and managing data related to Opportunities in a Kanban view within a Salesforce Lightning component.

Methods

getTopOpportunities()

Description: Retrieves a list of opportunities categorized by priority and stage. Also, provides a count of opportunities in each stage.

Return Type: OpportunityKanbanController.wrapper

updateOpprtuity(String recordId, String stageName)

Description: Updates the stage of an Opportunity given its record ID.

Parameters:

  • recordId (Type: String) – The ID of the Opportunity record to update.
  • stageName (Type: String) – The new stage name to set for the Opportunity.

Return Type: OpportunityKanbanController.wrapper

Inner Classes

wrapper

Description: A wrapper class to hold the response data for both opportunities and stage information.

Attributes:

  • opportunities (Type: List<Opportunity>) – List of Opportunity records.
  • pickVals (Type: List<OpportunityKanbanController.stageObject>) – List of stageObject instances containing stage names and counts.

stageObject

Description: Represents a stage in the Kanban view along with the number of opportunities in that stage.

Attributes:

  • stage (Type: String) – The name of the stage.

noRecords (Type: Integer) – The count of opportunities in the stage.