DataTable Component in Lightning Flows : jayakrishnasfdc

DataTable Component in Lightning Flows
by: jayakrishnasfdc
blow post content copied from  Jayakrishna Ganjikunta
click here to view original post


We all know that we can use Lightning Components in our flows to enhance the user interface in the different flow screens. We can also use Apex to create advance functionality which flow can invoke.

And we also know that we have Lightning Data table to use Lightning components and LWC but cannot in flows, which we have to achieve by embedding a lightning component in flows which I am explaining through this blog post.


Use Case:

Display the list of Contact with particular Type or any other filter criteria on any object where we have lookup relationship with. So the means we need to display contact records data table. and after selecting required contact on screen do update address or any other fields on the lookup object with selected contact Data.

For the above example , below are the Component i am going to implement

  1. Field set on Contact
  2. Aura Component
  3. Apex Class
  4. Apex Test Class
  5. Lightning Flow
  6. Quick Action Button

Note: This design and components are scalable , can be reused with different objects with same kind of requirement.

Development:

  1. Field Set

Create a field set(ContactFieldForFlowTable) on contact object and add the fields which we want to display as columns on the screen when we click on the button.

2. Aura Component Bundle

Create a new Lightning/Aura Component(FlowTableComponent) for UI design to pass the attributes and client side controller data to apex (server side).

FlowTableComponent.cmp

<aura:component controller="FlowTableController" access="global" implements="lightning:availableForFlowScreens">
    <aura:attribute name="FieldLabels" type="List"/>
    <aura:attribute name="TableRows" type="List"/>
    <aura:attribute name="ObjectAPIName" type="String"/>
    <aura:attribute name="WhereClause" type="String"/>
    <aura:attribute name="FieldSetName" type="String"/>
    <aura:attribute name="SelectedRecordId" type="String"/>
    <aura:attribute name="AllowSelection" type="Boolean"/>
    <aura:attribute name="NoRecords" type="Boolean" default="false"/>
    <aura:attribute name="NoRecordsMessage" type="String" default="No Billing and Shipping Contacts to Display" />
    <aura:html tag="style">
        .slds-modal__container {
        width : 95% !important;
        max-width : 1460px !important;
        }
    </aura:html>
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <aura:renderIf isTrue="{!not(v.NoRecords)}">
        <!-- <table class="slds-table slds-table_bordered slds-table_striped slds-table_fixed-layout slds-scrollable" style="border: 1px solid rgb(217, 219, 221);"> -->
        <div class="slds-scrollable_x">
                <table class="slds-table slds-table_cell-buffer slds-no-row-hover slds-table_bordered slds-table_striped slds-table_col-bordered">
            
            
            <thead class="thead-cls">   
                <tr class="slds-text-title_caps">
                    <aura:iteration items="{!v.FieldLabels}" var="field_label" >
                        <th>
                            <div class="slds-truncate slds-text-body_large">{!field_label}</div>
                        </th>
                    </aura:iteration>
                </tr>
            </thead>
            <tbody class="tbody-cls">
                <aura:iteration items="{!v.TableRows}" var="row" indexVar="index">
                    <tr  class="slds-hint-parent" data-myid="row" data-index="{!index}" aura:id="{!index}" onclick="{!c.onClickHandle }">
                        <aura:iteration items="{!row.Fields}" var="field">
                            <td class="slds-truncate">
                                <lightning:layout>
                                    <aura:renderIf isTrue="{!and(v.AllowSelection, field.FirstField)}">
                                        <lightning:layoutItem class="slds-m-right_x-small">
                                            <lightning:input type="radio" label="" name="SelectedRecord" value="{!row.RecordId}" variant="label-hidden" onchange="{!c.handleRadioClick}"/>
                                        </lightning:layoutItem>
                                    </aura:renderIf>
                                    <lightning:layoutItem class="slds-truncate">
                                        <aura:renderIf isTrue="{!(field.FieldType == 'BOOLEAN')}">
                                            <aura:if isTrue="{!field.Value == 'true'}">
                                                <lightning:input type="checkbox" disabled="true" label=""  checked="true" variant="label-hidden"/>
                                                <aura:set attribute="else">
                                                    <lightning:input type="checkbox" disabled="true" label=""  checked="false" variant="label-hidden"/>
                                                </aura:set>
                                            </aura:if> 
                                        </aura:renderIf>
                                        <aura:renderIf isTrue="{!(field.FieldType == 'CURRENCY')}">
                                            <lightning:formattedNumber value="{!field.Value}" style="currency" maximumFractionDigits="2"/>
                                        </aura:renderIf>
                                        <aura:renderIf isTrue="{!(field.FieldType == 'PERCENT')}">
                                            <lightning:formattedNumber value="{!field.Value}" style="percent" maximumFractionDigits="2"/>
                                        </aura:renderIf>
                                        <aura:renderIf isTrue="{!(field.FieldType == 'DOUBLE' || field.FieldType == 'LONG' || field.FieldType == 'INTEGER')}">
                                            <lightning:formattedNumber value="{!field.Value}" style="decimal" maximumFractionDigits="2"/>
                                        </aura:renderIf>
                                        <aura:renderIf isTrue="{!(field.FieldType == 'PHONE')}">
                                            <lightning:formattedPhone value="{!field.Value}"/>
                                        </aura:renderIf>
                                        <aura:renderIf isTrue="{!(field.FieldType == 'DATE' || field.FieldType == 'TIME' || field.FieldType == 'DATETIME' || field.FieldType == 'STRING' || field.FieldType == 'PICKLIST' || field.FieldType == 'MULTIPICKLIST' || field.FieldType == 'ADDRESS' || field.FieldType == 'ID' || field.FieldType == 'REFERENCE' || field.FieldType == 'EMAIL')}">
                                            {!field.Value}
                                        </aura:renderIf>
                                        <aura:renderIf isTrue="{!(field.FieldType == 'TEXTAREA')}">
                                            <aura:unescapedHtml value="{!field.Value}"/>
                                        </aura:renderIf>
                                        <aura:renderIf isTrue="{!(field.FieldType == 'URL')}">
                                            <lightning:formattedUrl value="{!field.Value}" />
                                        </aura:renderIf>
                                    </lightning:layoutItem>
                                </lightning:layout>
                            </td>
                        </aura:iteration>
                    </tr>
                </aura:iteration>
            </tbody>
        </table>
        </div>
        
        <aura:set attribute="else">
            <div class="slds-align_absolute-center">
                {!v.NoRecordsMessage}
            </div>
        </aura:set>
    </aura:renderIf>
    
</aura:component>

FlowTableComponentController.js

({
        doInit : function (component, event, helper) {
                helper.doInit(component, event, helper);
        },

        handleRadioClick : function (component, event, helper) {
                let selected_record_id = event.getSource().get('v.value');
                component.set('v.SelectedRecordId', selected_record_id);
        },
    onClickHandle : function (component, event, helper) {
        console.log('handle function');
        let auraId = event.currentTarget.dataset.index;
        let compoValue = component.find(auraId);
        
        }       
})

FlowTableComponentHelper.js

({
        doInit : function (component, event, helper) {
         let action = component.get('c.getRecordsToDisplayInTable');
        action.setParams({
            sobject_name: component.get('v.ObjectAPIName'),
            field_set_name: component.get('v.FieldSetName'),
            where_clause: component.get('v.WhereClause')
        });
        action.setCallback(this, function(response){
            let state = response.getState();
            if(state === 'SUCCESS'){
                let apex_method_result = response.getReturnValue();
                if(apex_method_result.Success){
                                        if(apex_method_result.TableRows.length != 0){
                                                component.set("v.NoRecords", false);
                                                component.set("v.FieldLabels", apex_method_result.FieldLabels);
                                                component.set("v.TableRows", apex_method_result.TableRows);
                    } else{
                        component.set("v.NoRecords", true);
                    }
                } else {
                    helper.handleError(component, apex_method_result.ErrorMessage);
                }
            } else if(state === 'ERROR'){
                helper.handleError(component, response.getError());
            }
        });
        $A.enqueueAction(action);
    },

        handleError : function (component,error) {
                component.set("v.NoRecordsMessage", error);
        }
})

FlowTableComponent.css

.THIS {
}
.THIS .slds-checkbox [type="checkbox"]:checked + .slds-checkbox_faux::after, 
.THIS .slds-checkbox [type="checkbox"]:checked + .slds-checkbox--faux::after, 
.THIS .slds-checkbox [type="checkbox"]:checked ~ .slds-checkbox_faux::after, 
.THIS .slds-checkbox [type="checkbox"]:checked ~ .slds-checkbox--faux::after, 
.THIS .slds-checkbox [type="checkbox"]:checked + .slds-checkbox__label .slds-checkbox_faux::after, 
.THIS .slds-checkbox [type="checkbox"]:checked + .slds-checkbox__label .slds-checkbox--faux::after {
    border-bottom: 2px solid #070707 !important;
    border-left: 2px solid #070707 !important;
}
.THIS .thead-cls tr>th:first-child, .THIS .tbody-cls tr>td:first-child {
  position: sticky;
  z-index: 1;
  top: 0;
  left: 0;
  background: #fafafa;
}

FlowTableComponent.design

<design:component>
        <design:attribute name="ObjectAPIName" label="Object API Name" />
    <design:attribute name="WhereClause" label="Where Clause" />
    <design:attribute name="FieldSetName" label="Field Set Name" />
    <design:attribute name="SelectedRecordId" label="Selected Record Id" />
    <design:attribute name="AllowSelection" label="Allow Selection" />
    <design:attribute name="NoRecordsMessage" label="No Records Message" default="There are no records to display"/>
</design:component>

3. Apex Class

Create an apex class Controller(FlowTableController) to get the data from salesforce in the backend.

FlowTableController.apxc

public class FlowTableController  {
    
    @AuraEnabled
    public static ApexMethodResult getRecordsToDisplayInTable(String sobject_name, String field_set_name, String where_clause){
        
        //Properties
        ApexMethodResult apex_method_result = new ApexMethodResult();
        
        try {
            Schema.FieldSet field_set = getFieldSetForObject(sobject_name, field_set_name);
            apex_method_result.setFieldLabels(field_set);
            String query = getQueryForOjbectFieldSetAndWhereClause(sobject_name, field_set, where_clause);
            List<sObject> table_records = Database.query(query);

            for(sObject table_record : table_records){
                TableRow table_row = new TableRow(table_record, field_set);
                apex_method_result.TableRows.add(table_row);
            }
        } catch(Exception e){
            apex_method_result.handleException(e);
        }
        return apex_method_result;
    }
    
    private static Schema.FieldSet getFieldSetForObject(String sobject_name, String field_set_name){
        Map<String, Schema.SObjectType> global_describe = Schema.getGlobalDescribe();
        
        if(!global_describe.containsKey(sobject_name)){
            throw new FlowTableSelectionException('Bad object specified ' + sobject_name);
        }
        
        Schema.SObjectType sobject_type = global_describe.get(sobject_name);
        Schema.DescribeSObjectResult sobject_type_describe = sobject_type.getDescribe();
        if(!sobject_type_describe.FieldSets.getMap().containsKey(field_set_name)){
            throw new FlowTableSelectionException('Can\'t find fieldset ' + field_set_name);
        }
        return sobject_type_describe.FieldSets.getMap().get(field_set_name);
    }
    
    private static String getQueryForOjbectFieldSetAndWhereClause(String sobject_name, Schema.FieldSet field_set, String where_clause){
        List<String> fields_api_name = new List<String>();
        String AccId = '';  
            List<String> lsp = where_clause.split('=');
            String recId = lsp[1];
            recId = recId.replaceAll('\'', '');
            recId = recId.replaceAll(' ', '');
             Id recId1 = Id.valueOf(recId);
            String sObjName = recId1.getSObjectType().getDescribe().getName();
        
        for(Schema.FieldSetMember fieldset_member : field_set.getFields()){
            fields_api_name.add(fieldset_member.getFieldPath());
        }
        
        /* String query = 'SELECT ' + String.join(fields_api_name, ', ') + ' FROM ' + sobject_name;
            if(!String.isBlank(where_clause)){
            query += ' WHERE ' + where_clause;
        }  */
         String query1='';

         If(sObjName=='Order'){
        query1 = 'Select id,AccountId from '+ sObjName + ' where ' + where_clause;
        List<Order> VarObj = Database.query(query1);  
            if(VarObj.size() > 0) {
            AccId = String.valueOf(VarObj[0].AccountId);
        }
        }
         If(sObjName=='blng__Invoice__c'){
        query1 = 'Select id,blng__Account__c from '+ sObjName + ' where ' + where_clause;
        List<blng__Invoice__c> VarObj = Database.query(query1);  
            if(VarObj.size() > 0) {
            AccId = String.valueOf(VarObj[0].blng__Account__c);
        }
        }
        
        String query = 'SELECT ' + String.join(fields_api_name, ', ') + ' FROM Contact ';
        if(!String.isBlank(AccId)){
            query += ' WHERE AccountId= \''+ AccId +'\' AND Type__c = \'Billing and Shipping\' Order by FirstName';
        }
        System.debug('Query:'+ query);
        return query;
    }
    
    @TestVisible
    private class ApexMethodResult {
        @AuraEnabled
        public List<TableRow> TableRows;
        @AuraEnabled
        public List<String> FieldLabels;
        @AuraEnabled
        public Boolean Success;
        @AuraEnabled
        public String ErrorMessage;
        
        public ApexMethodResult(){
            this.Success = true;
            this.TableRows = new List<TableRow>();
            this.FieldLabels = new List<String>();
        }
        
        public void handleException(Exception e){
            this.Success = false;
            this.ErrorMessage = e.getMessage();
        }
        
        public void setFieldLabels(Schema.FieldSet field_set){
            for(Schema.FieldSetMember fieldset_member : field_set.getFields()){
                this.FieldLabels.add(fieldset_member.getLabel());
            }
        }
    }
    
    @TestVisible
    private class TableRow {
        @AuraEnabled
        public List<Field> Fields;
        @AuraEnabled
        public String RecordId;
        
        public TableRow(sObject record, Schema.FieldSet field_set){
            this.RecordId = record.Id;
            this.Fields = new List<Field>();
            for(Schema.FieldSetMember fieldset_member : field_set.getFields()){
                Field table_row_field = new Field(record, fieldset_member, this.Fields.isEmpty());
                this.Fields.add(table_row_field);
            }
        }
    }
    
    @TestVisible
    private class Field {
        @AuraEnabled
        public String Value;
        @AuraEnabled
        public String FieldType;
        @AuraEnabled
        public Boolean FirstField;
        
        public Field(sObject record, Schema.FieldSetMember fieldset_member, Boolean first_field){
            this.FirstField = first_field;
            String field_api_name = fieldset_member.getFieldPath();
            this.FieldType = String.valueOf(fieldset_member.getType());
            if(record.get(field_api_name) != null){
                if(this.FieldType == 'DATE'){
                    this.Value = ((Date)record.get(field_api_name)).format();
                } else if (this.FieldType == 'DATETIME'){
                    this.Value = ((DateTime)record.get(field_api_name)).format();
                } else if (this.FieldType == 'PERCENT'){
                    this.Value = String.valueOf((Decimal)record.get(field_api_name) / 100.0);
                } else {
                    this.Value = String.valueOf(record.get(field_api_name));
                }
            }
        }
    }
    
    @TestVisible
    private class FlowTableSelectionException extends Exception {}
}

4. Apex Test Class

Write the Test class(FlowTableControllerTest) for the above apex.

FlowTableControllerTest.apxc

@isTest
private class FlowTableControllerTest {
    
    @testSetup static void setupData() {
        Opportunity test_opportunity = new Opportunity(CloseDate = Date.today(), Name ='Test Opportunity', StageName = 'Prospecting');
        insert test_opportunity;
        List<Quote> test_quotes = new List<Quote>();
        for(Integer i = 0; i < 3; i++) {
            Quote test_quote = new Quote(Name = 'Test Quote ' + i, OpportunityId = test_opportunity.Id);
            test_quotes.add(test_quote);
        }
        insert test_quotes;        
    }
    
    @isTest
    static void testGetRecordsToDisplayInTableReturnTableRowsForOpportunityQuotes() {
        Opportunity test_opportunity = [SELECT Id FROM Opportunity];
        
        Account account = new Account(Name = 'Test Account'); 
        insert account;
        
        Contact contact = new Contact(FirstName = 'Test Contact', LastName = 'ContactLast', AccountId = account.Id);
        insert contact;
        
        String where_clause = 'AccountId = \'' + account.Id + '\'';
        String sobject_name = 'Contact';
        String field_set_name = 'ContactFieldForFlowTable';
        
        Test.startTest();
        FlowTableController.ApexMethodResult result = FlowTableController.getRecordsToDisplayInTable(sobject_name, field_set_name, where_clause);
        Test.stopTest();
        
        system.debug('result####'+ result);
        System.assertEquals(true, result.Success);
        System.assertEquals(1, result.TableRows.size());
    }
    
    @isTest
    static void testGetRecordsToDisplayInTableReturnErrorForBadOjbect() {
        Opportunity test_opportunity = [SELECT Id FROM Opportunity];
        String where_clause = 'OpportunityId = \'' + test_opportunity.Id + '\'';
        String sobject_name = 'Bad';
        String field_set_name = 'QuoteFieldForFlowTable';
        
        Test.startTest();
        FlowTableController.ApexMethodResult result = FlowTableController.getRecordsToDisplayInTable(sobject_name, field_set_name, where_clause);
        Test.stopTest();
        
        System.assertEquals(false, result.Success);
        System.assertEquals('Bad object specified Bad', result.ErrorMessage);
        System.assertEquals(0, result.TableRows.size());
    }
    
    @isTest
    static void testGetRecordsToDisplayInTableReturnErrorForBadFieldSet() {
        Opportunity test_opportunity = [SELECT Id FROM Opportunity];
        String where_clause = 'OpportunityId = \'' + test_opportunity.Id + '\'';
        String sobject_name = 'Quote';
        String field_set_name = 'Bad';
        
        Test.startTest();
        FlowTableController.ApexMethodResult result = FlowTableController.getRecordsToDisplayInTable(sobject_name, field_set_name, where_clause);
        Test.stopTest();
        
        System.assertEquals(false, result.Success);
        System.assertEquals('Can\'t find fieldset Bad', result.ErrorMessage);
        System.assertEquals(0, result.TableRows.size());
    }
    
    @isTest
    static void testGetRecordsToDisplayInTableReturnNoResultsQuotes() {
       Opportunity test_opportunity = [SELECT Id FROM Opportunity];
        
        Account account = new Account(Name = 'Test Account'); 
        insert account;
        
        Contact contact = new Contact(FirstName = 'Test Contact', LastName = 'ContactLast', AccountId = account.Id);
        insert contact;
        
        String where_clause = '';
        String sobject_name = 'Contact';
        String field_set_name = 'ContactFieldForFlowTable';
        
        Test.startTest();
        FlowTableController.ApexMethodResult result = FlowTableController.getRecordsToDisplayInTable(sobject_name, field_set_name, where_clause);
        Test.stopTest();
        
        //Assert for result
        System.assertEquals(false, result.Success);
    }
    
}

5. Lightning Flow

Create an Screen Lightning flow (UpdateAddress) as explained below.

This example I have designed with different types of Contact Types , but generally it depends on your requirement.

Create WhereClauseFormula, to pass the Id from flow to Aura Component, like below to split , that will help to find out id and sobject as per design in the above Aura and Apex calss.

Create one Text variable as like below

Create an Assignment to assign Whereclausformula to variable.

Get Order Records based on Record Id.

Below ScreenFlow element is important in entire flow. where we added FlowTableLightningComponent and Fieldset.
Provide values in attributes as per required in screenflowcomponent.

‘Selected Record Id’ where we get the contact Id from Aura & Apex to Flow which is nothing but selected contact by user on screen which we consider to update order address with selected contact address.

Create one ‘Get Contact’ element.

Update Order with Selected contact fields with mapping set in Update Order element.

6. Quick Action Button

Now we need to create an quick action on the lookup(Target Object) on where we need to display list of contacts. For example Order. On Order We have a lookup field with Contact.

Create a Quick action Button ‘Update Address’ and call the Lightning flow we created in earlier step.

Output:

Click on the Button Update Address
here is the output Record Data Table.

Thanks for Reading.
Jayakrishna


May 02, 2021 at 04:17PM
Click here for more details...

=============================
The original post is available in Jayakrishna Ganjikunta by jayakrishnasfdc
this post has been published as it is through automation. Automation script brings all the top bloggers post under a single umbrella.
The purpose of this blog, Follow the top Salesforce bloggers and collect all blogs in a single place through automation.
============================

Salesforce