UI Architecture
In the world of Salesforce development, user interface (UI) plays a vital role in creating a seamless and intuitive experience for users. Although the Salesforce platform offers a wide range of pre-built UI components, there are instances when you may need to develop your own UI components in order to suit certain business needs.
In this blog, we will discuss the power of Salesforce Lightning Components, and we'll also learn how to create a lightning component and integrate it with the backend.
Let's explore the project's key feature, which is that it is entirely dynamic.
The Grace of Dynamization
Users can select options from a dropdown menu to filter cases based on specific criteria ("SBU" - Service Business Unit).
Upon selection, the component updates and displays relevant case information. The brilliance of this project can be seen in the way that the above snippet shows different interfaces for AVAF Entities and the below snippet shows different interfaces for My Cases.
If you are curious to know more about Dynamic Tabs –then 👉👉👉 click here
ficCenter.js
The "FicCenter" Lightning Web Component enhances case management within Salesforce by providing a user-friendly interface with features like dropdown filtering, tab-based navigation, pagination, global and local search, and queue-specific actions. It aims to streamline case handling and improve user efficiency within the Salesforce platform.
import {LightningElement, wire, track, api} from 'lwc';
import {addTabNameAndURL,getDisplayTabName,getCaseUrl,getUrl,getTotalCases} from 'c/recordProcessor'
import {columns, queueWithActiveValue, searchfields} from 'c/constant'
import {handleLocalSearch} from 'c/search'
import getDropDownOptions from '@salesforce/apex/CaseController.getDropDownOptions';
import getTabListWithQueue from '@salesforce/apex/CaseController.getWrapCases';
importgetWrapCasesForSelectedQueue from '@salesforce/apex/CaseController.getWrapCasesForSelectedQueue';
import getCases from '@salesforce/apex/CaseController.findCases';
export default class FicCenter extends LightningElement {
options =[];
activeTab;
columns;
queueWithActiveValue;
searchfields;
selectedQueue;
totalPage;
totalRecords ;
currentPage ;
selectedQueueCases;
offsetValue = 0;
pageNumber = 0;
tabsData;
pageSize = 1;
SBU = 'AVAF';
globalSearchKey;
localSearchKey;
casesToBeDisplay = [];
copyCasesToBeDisplay = [];
filterGlobalCases = [];
localSearchRecords = [];
isFilterGlobalCases;
globalSearchTable;
selectedOption = '';
data;
cases;
isCases;
isLoaded = false;
buttonValue=true;
isCases = false;
@wire (getDropDownOptions, {SBU: '$SBU', pageSize: '$pageSize'})
getData({data, error}) {
if (data) {
this.columns = columns;
this. queueWithActiveValue = queueWithActiveValue;
this.searchfields = searchfields;
this.initializeData(data);
this.hideSpinner();
}
else if (error) {
console.log('Error: '+error);
}
}
initializeData(data) {
this.data = data;
this.resetData(data);
this.selectedOption = data.dropdownOptions[0];
this.options = this.getDropDownOption(data);
}
resetData(data) {
this.tabsData = addTabNameAndURL(data);
}
setDisplayCases(cases) {
this.casesToBeDisplay = [...cases];
this.copyCasesToBeDisplay = [...this.casesToBeDisplay];
}
pagination() {
this.totalRecords = getTotalCases(this.tabsData, this.selectedQueue);
this.setPagination();
}
setPagination() {
this.totalPage = this.totalRecords%this.pageSize == 0 ? this.totalRecords/this.pageSize : parseInt(this.totalRecords/this.pageSize) +1;
}
getDropDownOption(data) {
return data.dropdownOptions.map(option => {
return this.getWrappedOption(option)
});
}
getWrappedOption(option) {
return {
label: option,
value : option
};
}
showSpinner() {
this.isLoaded = false;
}
hideSpinner() {
this.isLoaded = true;
}
handleOptionChange(event) {
this.selectedOption = event.detail.value;
this.showSpinner();
getTabListWithQueue({
SBU:this.SBU,
selectedOption: this.selectedOption,
offsetValue: this.offsetValue,
pageSize: this.pageSize
})
.then((result) => {
console.log('Data: '+JSON.stringify(result));
this.resetData(result);
});
this.hideSpinner();
}
getCasesForActiveTab() {
this.tabsData.forEach((value)=> {
if (value.queueName == this.selectedQueue) {
this.cases = [...value.cases];
}
});
}
handleActiveTab(event) {
this.setCurrentPageAndPageNumber();
this.selectedQueue = event.target.value;
this.activeTab = event.target.title;
this.buttonValue = this.queueWithActiveValue.get(this.selectedQueue);
this.getOffsetValue();
this.getCasesForActiveTab();
this.setDisplayCases(this.cases);
this.pagination();
this.isCases = this.casesToBeDisplay.length > 0 ? true : false;
}
setCurrentPageAndPageNumber() {
this.currentPage = 1;
this.pageNumber = 0;
}
handleNext() {
++this.pageNumber;
++this.currentPage;
this.getOffsetValue();
this.getSelectedQueueCases();
}
handlePrevious() {
--this.pageNumber;
--this.currentPage;
this.getOffsetValue();
this.getSelectedQueueCases();
}
getdisableNext() {
return this.currentPage <= 1;
}
getdisablePrevious() {
return this.currentPage >= this.totalPage;
}
getOffsetValue() {
this.offsetValue = this.pageNumber * this.pageSize;
}
getSelectedQueueCases() {
getWrapCasesForSelectedQueue({
queueName: this.selectedQueue,
tabName: this.activeTab,
offsetValue: this.offsetValue,
pageSize: this.pageSize
})
.then((result) => {
this.setDisplayCases(this.updateQueueCases(result).cases);
});
}
updateQueueCases(result) {
return getCaseUrl(result);
}
handleSearchKey(event) {
this.globalSearchKey = event.target.value;
if (this.globalSearchKey == 0) {
this.resetGlobalSearchField();
}
}
resetGlobalSearchField() {
this.isFilterGlobalCases = false;
this.globalSearchKey = '';
}
handleGlobalSearch() {
this.isFilterGlobalCases = true;
getCases({ searchKeyWord: this.globalSearchKey })
.then((result) => {
this.filterGlobalCases = result.slice().map(
value => {
return getUrl(value);
}
);
console.log('Search value: '+JSON.stringify(this.filterGlobalCases));
})
.catch((error) => {
this.error = error;
});
}
handleLocalSearchKey(event) {
this.localSearchKey = event.target.value;
this.casesToBeDisplay = handleLocalSearch(this.copyCasesToBeDisplay, this.localSearchKey, this.searchfields);
this.totalRecords = this.casesToBeDisplay.length;
letpage =this.currentPage;
letpageNumber = this.pageNumber;
this.setCurrentPageAndPageNumber();
this.setPagination();
if (this.localSearchKey.length == 0) {
this.pagination();
this.pageNumber = pageNumber;
this.currentPage = page
this.resetLocalSearchField();
}
}
resetLocalSearchField() {
this.casesToBeDisplay = [...this.copyCasesToBeDisplay];
}
}
Searching
The component supports global and local search functionality, allowing users to search for records based on specific criteria.
Global Search: In global search, it will search across all salesforce cases.
Local Search: In local search, it will search only in selected tab and queue cases. Basically, The handleLocalSearch function filters an array of records (filterRecord) based on a local search key (localSearchKey) and a set of search fields (searchfields). It does so by iterating through each object in the filterRecord array and checking if any of the specified search fields contain a case-insensitive match with the localSearchKey. The filtered results are returned as a new array.
Search.js
const handleLocalSearch = (filterRecord, localSearchKey, searchfields) => {
letfilteredRecord = filterRecord.slice().filter(
(object)=> {
return searching(object, localSearchKey, searchfields);
});
return filteredRecord;
}
const searching = (object, localSearchKey, searchfields) => {
letresult = false;
for (let i=0; i<searchfields.length; i++) {
if (object[searchfields[i]] && (object[searchfields[i]].toString().toLowerCase())
.includes(localSearchKey.toLowerCase())) {
result = true;
break;
}
}
return result;
}
export {handleLocalSearch}
ficCenter.html
This HTML file represents a complex Salesforce Lightning component or page with conditional rendering, tabbed navigation, data tables, search functionality, and custom Lightning components. It's likely part of a larger Salesforce application for managing and displaying data.
<template>
<div>
<lightning-layout class="slds-grid">
<lightning-layout-item class="globalSearch">
<lightning-input label="" type="search" value={globalSearchKeys} onchange={handleSearchKey} placeholder="Track and trace-search by Case number, CIF, CASA reference, CASA sequence or status"></lightning-input>
</lightning-layout-item>
<lightning-layout-item class="searchButton">
<lightning-button variant="brand" label="Search" onclick={handleGlobalSearch}></lightning-button>
</lightning-layout-item>
</lightning-layout>
</div>`
<template if:true={isFilterGlobalCases}>
<lightning-datatable key-field="id" data={filterGlobalCases} columns={columns}></lightning-datatable>
</template>
<div>
<lightning-combobox label="SBU" value={selectedOption} placeholder="" options={options} onchange={handleOptionChange} ></lightning-combobox>
</div>
<template if:true={isLoaded}>
<template if:true={tabsData}>
<lightning-tabset>
<template for:each={tabsData} for:item="tab">
<lightning-tab label={tab.displayTab} key={tab.queueName} value={tab.queueName} title={tab.tabName} onactive={handleActiveTab}>
<template if:true={isCases}>
<c-fic-start-processing button-value={buttonValue} tab={tab} cases={casesToBeDisplay}></c-fic-start-processing>
<lightning-layout class="localSearch">
<lightning-layout-item>
<lightning-input label="" type="search" value={localSearchKey} onchange={handleLocalSearchKey} placeholder="Track and trace-search by Case number, CIF, CASA reference, CASA sequence or status"></lightning-input>
</lightning-layout-item>
</lightning-layout>
<lightning-card>
<div>
<lightning-datatable key-field="id" data={casesToBeDisplay} columns={columns}></lightning-datatable>
</div>
<div slot="footer" class="slds-var-m-vertical_medium">
<div class="slds-align_absolute-center">
<div class="slds-p-right_xx-small">
<lightning-button label="Previous" variant="brand" icon-name="utility:back" onclick={handlePrevious} disabled={disableNext}></lightning-button>
</div>
<span class="slds-badge slds-badge_lightest">
<p>{currentPage} of {totalPage}</p>
</span>
<div class="slds-p-left_xx-small">
<lightning-button label="Next" variant="brand" icon-name="utility:forward" icon-position="right" name="next" onclick={handleNext} disabled={disablePrevious}></lightning-button>
</div>
</div>
</div>
</lightning-card>
</template>
</lightning-tab>
</template>
</lightning-tabset>
</template>
</template>
<div if:false={isLoaded}>
<lightning-spinner alternative-text="Loading..." variant="brand"></lightning-spinner>
</div>
</template>
ficCenter.css
.globalSearch {
padding-left: 0px;
width: 85%;
}
.searchButton {
margin-top: 20px;
margin-left: 10px;
}
.localSearch {
display: block;
}
.iconMargin{
margin-top: 2px;
}
recordProcessor.js
This code defines a set of functions that process and transform data related to cases and tabs.
addTabNameAndURL(data): This function takes a piece of data, likely containing information about cases, and for each case, it calculates and adds a display tab name along with its URL. It does this by calling the getDisplayTabName function for each case.
getDisplayTabName(value): Given a case, this function processes it by getting its URL using the getCaseUrl function and then adds a displayTab property with the tab name. It returns the processed case.
getCaseUrl(value): This function takes a case and processes it by adding a URL property. It also processes each element in the cases array within the case by calling the getUrl function to generate URLs for them.
getUrl(value): This function generates a URL for a case based on its ID. It also adds additional properties like AccountName and LastModifiedName by looking up related information. The URL is constructed to link to the case's details.
getTabName(value): Given a value, likely representing a tab, this function generates a display name for the tab by combining its name with the total number of cases it contains.
getTotalCases(data, selectedQueue): This function calculates and returns the total number of cases in a specific queue.
const addTabNameAndURL = (data) => {
return data.wrappedCases.map((value) => {
return getDisplayTabName(value);
});
}
const getDisplayTabName = (value) => {
let processedCases = getCaseUrl(value);
return {
...processedCases,
displayTab : getTabName(value)
};
}
const getCaseUrl = (value) => {
let val= {...value};
val.cases = value.cases.map(element => {
return getUrl(element);
});
return val;
}
const getUrl = (value) => {
return {
...value,
AccountName: value.Account ? value.Account.Name : null,
LastModifiedName: value.LastModifiedBy ? value.LastModifiedBy.Name : null,
URL : `/lightning/r/Case/${value.Id}/view`
}
}
const getTabName = (value) => {
return value.tabName + '('+ value.totalCases + ')';
}
const getTotalCases = (data, selectedQueue) => {
let totalCases;
data.forEach((value) => {
if (value.queueName == selectedQueue) {
totalCases = value.totalCases;
}
});
return totalCases;
}
export {
addTabNameAndURL,
getDisplayTabName,
getCaseUrl,
getTotalCases,
getUrl
}
ficStartProcessing.js
ficStartProcessing.html
This HTML template is designed to display information about a case queue, including its name, the number of cases, and a button to start processing. It uses Salesforce Lightning Design System components for styling and functionality within a Lightning Web Component.
<template>
<div class="slds-box slds-no-flex borderClass" style="margin-bottom: 20px;">
<lightning-card >
<div>
<lightning-icon icon-name="standard:case" alternative-text="Case" title="Case"></lightning-icon>
<span class="queueClass">{tab.tabName}</span>
<p class="slds-p-horizontal_small paragraphClass"> {tab.totalCases} item | Sorted by date and time received</p>
</div>
<div>
<lightning-button class="buttonClass" label="Start Processing" onclick={handleChangeCaseOwner} disabled={buttonValue}></lightning-button>
</div>
</lightning-card>
</div>
</template>
ficStartProcessing.css
Conclusion
In this project, we aimed to create a dynamic tab component that adjusts its content based on user choices. Users can switch between dropdown and tab views, highlighting the active tab for clarity. Our primary goal was to make the tabs and dropdowns fully dynamic and configurable through the user interface. Additionally, we explored pagination and search features to enhance their mobility.
Integration
Now it's time to tie everything together.
Part 1 Recap
In the first section, we made an effort to understand the user's needs and to talk about potential approaches to finding an efficient solution. We began by creating the custom application "Avenoir". Our primary goal was to make our solution more dynamic and user-friendly so that anyone can customize it from the UI without editing the code. To provide our project with additional dynamic functionality, we built custom metadata that should be transferable between sandboxes as well.
Part 2 Recap
In the second part, we discussed the backend architecture and the architectural patterns we should use. We used the Enterprise Architect pattern because it will make our project more scalable, flexible, and easily customized.
For each operation, we generated a different class: a domain class for SOQL queries, a processor class to process records, a wrapper class to combine the data into a single object, and so on.
Important Note
As this is the last part of the blog be mindful, when you are deploying this project in your organization or taking reference from this project please start from Part 1 and then go to Part 2 and then Part 3. Please follow the instructions step by step as given in the blog.
Happy Coding! You can leave a comment to help me understand how the blog helped you. If you need further assistance, please contact us. You can click "Reach Us" on the website and share the issue with me.
Blog Credit:
A. Kumar
Salesforce Developer
Avenoir Technologies Pvt. Ltd.
Reach us: team@avenoir.ai
Comments