+ Start a Discussion
Christian Schwabe 9Christian Schwabe 9 

LWC: Custom component merge custom and standard objects

Hello everyone,

i try to build a custom lightning web component for a generic way to merge custom and standardobjects like the standard merge functionality for account, contact and leads.

Therefore i built two lwcs:
  • potentialDuplicates
  • mergeDuplicates
potentialDupplicates show the result for the duplicate rules (DuplicateRule), duplicate recordset (DuplicateRecordSet) and duplicate record item (DuplicateRecordItem) and looks like the following one:
https://ibb.co/gwp1fRk (https://ibb.co/jMRKJZL)

The first lwc (german language) show the salesforce standard "potential duplicates" component.
Below that - the highlighted one is my custom lwc.

The first screen (step1) looks like the following (Unfortunately I can't upload images - sorry for that):
https://ibb.co/MkJmmZT

After the selection and click on "Next" second screen (step2) appears and looks like the follwing:
https://ibb.co/xXQZDV1

On screen2 you have to choose the masterrecord and fields which has to move to masterrecord.

For comparison: In the standard potential duplicates the screen for step2 looks like the following:
https://ibb.co/5x7hfFv

I want to do this the exact same way. Here comes my problem: I didn't find a structure provided by apexcontroller that is able to iterate in a way to this list that columns and rows are syncron. The prepared structure of (selected) data records should be combined in such a way that they can be passed on to an apex controller, which then takes over the merge process.

One of my various solutions provide the following structure: Map<Id, List<Map<String, String>>>. Where key of map is the record id. The value of the outer map is a list with a map (key: apiname, value: fieldvalue). But this structure does not fit to the columns of screen2 and you can't iterate over map. Also a template expression doesn't allow NumericLiteral to access a specific index or key. It seems like everything has to be a list.

If I can realize this list of the selection, the processing of the data sets in Apex is only a child's play. Today I have worried all day about it and have not found an adequate solution.

Any suggestions for a provided structure of objects, arrays, maps to rule this target?

After i finished that components and everything works fine I am willing to publish the corresponding components.
Christian Schwabe 9Christian Schwabe 9
Hi,

sorry for my missspelling. What I meant is the Iteration in <template> is not possible and it is not possible to reference and Index i.e. Index[0] in <template>.
Christian Schwabe 9Christian Schwabe 9
The idea to use a wrapper class has already come to me. But then I came across the question, how should a generic approach work with it? In the template I always have to know the name of the property of the object. There is no way to dynamically access a property in an object, e.g. obj['accountname'], If that were possible I could imagine an iteration over the list of API names, which does exactly the above described.
Christian Schwabe 9Christian Schwabe 9

One of first attempts for this solution looks like the follwing:
 
/**
     * Count Duplicate Rules (DuplicateRule) and associated Duplicate Record Sets (DuplicateRecordSet) to
     * determine how many duplicate records found, based on Duplicate Record Items (DuplicateRecordItem).
     *
     * @param   Id      recordId    Contains Id from current record view.
     *
     * @return  Map<Id, List<Map<String, String>>>  Outer-Map-Key: Id of record // Value of map is a list with a inner map (key: apiname, value: fieldvalue)
     *
     * The structure looks like the following: {
     *                                              "0011x00000KOMiJAAX": [
     *                                                  {
     *                                                      "Name": "Test GmbH"
     *                                                  },
     *                                                  {
     *                                                      "Industry": "Insurance"
     *                                                  }
     *                                              ]
     *                                           }
     */
    @AuraEnabled(cacheable=true)
    public static Map<Id, List<Map<String, String>>> getListOfDuplicateRecords(String recordId){
        String objectName = MetadataHelper.getObjectNameFromRecordIdPrefix(recordId);
        System.debug('>>>objectName: ' + objectName);

        List<DuplicateRule> listOfDuplicateRule = getDuplicateRules(objectName);
        System.debug('>>>listOfDuplicateRule.size(): ' + listOfDuplicateRule.size());

        List<DuplicateRecordSet> listOfDuplicateRecordSet = getDuplicateRecordSet(listOfDuplicateRule);

        List<DuplicateRecordItem> listOfDuplicateRecordItem = getDuplicateRecordItem(listOfDuplicateRecordSet);
        System.debug('>>>listOfDuplicateRecordItem: ' + listOfDuplicateRecordItem);

        List<Id> listOfRecordId = new List<Id>();
        for(DuplicateRecordItem duplicateRecordItem : listOfDuplicateRecordItem){
            System.debug('>>>duplicateRecordItem.RecordId: ' + duplicateRecordItem.RecordId);
            listOfRecordId.add(duplicateRecordItem.RecordId);
        }
        System.debug('>>>listOfRecordId: ' + listOfRecordId);

      
        List<String> listOfApiName = new List<String>();// {'Id', 'Name'}
        Map<String, Schema.SObjectField> standardAndCustomFields = MetadataHelper.getAllFields(objectName);
        for(String key : standardAndCustomFields.keySet()){
            Schema.DescribeFieldResult describeFieldResult = standardAndCustomFields.get(key).getDescribe();
            Boolean isAccessible = describeFieldResult.isAccessible();

            if(isAccessible){// Only fields which are accessible (FLS).
                String apiName = describeFieldResult.getName();

                listOfApiName.add(apiName);
                System.debug('>>>apiName: ' + apiName);
            }
        }
        System.debug('>>>listOfApiName: ' + listOfApiName);

        String query = 'SELECT ' + String.join(listOfApiName, ', ') + ' FROM ' + objectName + ' WHERE Id = :listOfRecordId';
        System.debug('>>>query: ' + query);

        Map<Id, List<Map<String, String>>> mapOfFieldsByRecordId = new Map<Id, List<Map<String, String>>>();
        for(SObject listOfSObject : Database.query(query)){

            List<Map<String, String>> listForMap = new List<Map<String, String>>();
            for(String apiName : listOfApiName){
                Map<String, String> mapOfValueByApiName = new Map<String, String>();
                String value = String.valueOf(listOfSObject.get(apiName));
                Boolean isBlank = String.isBlank(value);
                value = ((isBlank) ? '[empty]' : value);

                mapOfValueByApiName.put(apiName, value);
                listForMap.add(mapOfValueByApiName);
            }
            mapOfFieldsByRecordId.put((Id)listOfSObject.get('Id'), listForMap);
        }

        System.debug('>>>mapOfFieldsByRecordId: ' + mapOfFieldsByRecordId);
        return mapOfFieldsByRecordId;
    }

Than in js I extract the first row for the selection of master record in the following way:
/**
     * Is used to get header row for comparison.
     */
    _extractMasterRecordForComparison(){
        console.log('>>>_extractMasterRecordForComparison in mergeDuplicates.js called.');
        this.masterRecordsRow = [];
        
        this.selectedRows.forEach( recordId => {
            let record = this.convertedStructure[recordId];

            Object.keys(record).forEach( index => {// index acting as index based column.
                let row = record[index];

                Object.keys(row).forEach( apiName => {
                    console.log('>>>apiName: ' + apiName);

                    if(apiName === 'Name'){ // Column "Name" found.
                        this.masterRecordsRow.push({keyField: recordId, value: value});
                    }
                })
            })
        });

        /* console.log('>>>this.masterRecordsRow: ' + JSON.stringify(this.masterRecordsRow, null, '\t')); */
    }

This approch and retrieved structure from apex is NOT applicable for the other rows and columns to iterate over it and select field values to merge.
 
Alain CabonAlain Cabon
A common technique for LWC/Aura is also to "atomize" the template with smallest nested components that communicate together with events but here you are using a simple datatable that is not perhaps the best option but the fastest and easy to code instead of a component (table) with inner components (rows) with themselves inner component (cells ).
Rama ChunduRama Chundu
How are you able to invoke the standard View Duplicates page from you custom component? Or did you build the custom component for Potential Duplicate records as well?
Christian Schwabe 8Christian Schwabe 8
Hi Rama,

I didn't solve it. We use Duplicate Check (AppExchange: https://appexchange.salesforce.com/appxListingDetail?listingId=a0N300000058vzKEAQ) now.

Best regards,
Christian
Pavel ZaverachPavel Zaverach
Hi Christian Schwabe 8!!! I am creating the potential duplicate lwc component too. Can you share your example? I think you reached the best result according to your screen
Jenn H.Jenn H.
Hi, Christian Schwabe 8! I am also creating the potential duplicate lwc component. Can you please publish the corresponding components and the apex class handler? Thanks in advance.