Sample Code:
Apex Class:
public with sharing class OpportunityContactRoleController {
@AuraEnabled(cacheable=true)
public static OpportunityContactRoleWrapper fetchOpptyCons( String strRecordId ) {
OpportunityContactRoleWrapper objWrap = new OpportunityContactRoleWrapper();
List < ContactWrapper > availableCons = new List < ContactWrapper >();
Set < String > selectedCons = new Set < String >();
try {
Opportunity oppty = [ SELECT Id, AccountId FROM Opportunity WHERE Id =: strRecordId ];
Set < Id > setConIds = new Set < Id >();
for ( OpportunityContactRole objOCR : [ SELECT Id, ContactId, Contact.Name FROM OpportunityContactRole WHERE OpportunityId =: strRecordId ] ) {
ContactWrapper objCW = new ContactWrapper();
objCW.value = objOCR.ContactId;
objCW.label = objOCR.Contact.Name;
availableCons.add( objCW );
selectedCons.add( objOCR.ContactId );
setConIds.add( objOCR.ContactId );
}
for ( Contact objCon : [ SELECT Id, Name FROM Contact WHERE AccountId =: oppty.AccountId ] ) {
if ( !setConIds.contains( objCon.Id ) ) {
ContactWrapper objCW = new ContactWrapper();
objCW.value = objCon.Id;
objCW.label = objCon.Name;
availableCons.add( objCW );
}
}
} catch ( Exception e ) {
throw new AuraHandledException(e.getMessage());
}
objWrap.selectedCons = new List < String > ( selectedCons );
objWrap.availableCons = availableCons;
return objWrap;
}
@AuraEnabled
public static String addRemoveOppCons( String strRecordId, List < String > selectedCons, List < String > updatedCons ) {
try {
Map < Id, Id > mapConIdOCRId = new Map < Id, Id >();
List < OpportunityContactRole > listInsertOCR = new List < OpportunityContactRole >();
Set < Id > setOCRIds = new Set < Id >();
for ( OpportunityContactRole objOCR : [ SELECT Id, ContactId FROM OpportunityContactRole WHERE OpportunityId =: strRecordId ] ) {
mapConIdOCRId.put( objOCR.ContactId, objOCR.Id );
}
for ( String strConId : updatedCons ) {
if ( !selectedCons.contains( strConId ) ) {
listInsertOCR.add( new OpportunityContactRole( ContactId = strConId, OpportunityId = strRecordId ) );
}
}
for ( String strConId : selectedCons ) {
if ( !updatedCons.contains( strConId ) )
setOCRIds.add( mapConIdOCRId.get( strConId ) );
}
if ( listInsertOCR.size() > 0 ) {
insert listInsertOCR;
}
if ( setOCRIds.size() > 0 ) {
Database.delete( new List < Id > ( setOCRIds ), false );
}
return 'Successful';
} catch (Exception e) {
throw new AuraHandledException( e.getMessage() );
}
}
public class OpportunityContactRoleWrapper {
@AuraEnabled
public List < ContactWrapper > availableCons;
@AuraEnabled
public List < String > selectedCons;
}
public class ContactWrapper {
@AuraEnabled
public String value;
@AuraEnabled
public String label;
}
}
HTML:
<template>
<lightning-card>
<lightning-dual-listbox
class="slds-box"
name="OppCons"
label="Select/Remove Contacts"
source-label="Available Contact(s)"
selected-label="Selected Contact(s)"
options={availableCons}
value={selectedCons}
onchange={handleChange}>
</lightning-dual-listbox>
<p slot="footer">
<lightning-button
variant="brand"
label="Save"
onclick={saveChanges}
disabled={diableBool}>
</lightning-button>
</p>
</lightning-card>
</template>
JavaScript:
import { LightningElement, api, wire } from 'lwc';
import fetchOpptyCons from '@salesforce/apex/OpportunityContactRoleController.fetchOpptyCons';
import addRemoveOppCons from '@salesforce/apex/OpportunityContactRoleController.addRemoveOppCons';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class OpportunityContactRole extends LightningElement {
@api recordId;
availableCons;
selectedCons;
updatedCons;
diableBool = true;
@wire( fetchOpptyCons, { strRecordId: '$recordId' })
wiredRecs( { error, data } ) {
if ( data ) {
console.log( 'Records are ' + JSON.stringify( data ) );
this.availableCons = data.availableCons;
this.selectedCons = data.selectedCons;
} else if ( error ) {
console.log( 'Error ' + JSON.stringify( error ) );
}
}
handleChange( event ) {
this.diableBool = false;
const selectedOptionsList = event.detail.value;
console.log( 'Selected Options are ' + JSON.stringify( selectedOptionsList ) );
this.updatedCons = selectedOptionsList;
}
saveChanges() {
this.diableBool = true;
addRemoveOppCons( { strRecordId : this.recordId, selectedCons : this.selectedCons, updatedCons : this.updatedCons } )
.then( result => {
console.log( 'Result ' + JSON.stringify( result ) );
let message;
let variant;
if ( result === 'Successful' ) {
message = 'Successfully Processed!';
variant = 'success';
} else {
message = 'Some error occured. Please reach out to your Admin';
variant = 'error';
}
const toastEvent = new ShowToastEvent({
title: 'Opportunity Contact Add/Remove',
message: message,
variant: variant
});
this.dispatchEvent( toastEvent );
} )
.catch( error => {
console.log( 'Error ' + JSON.stringify( error ) );
} );
this.selectedCons = this.updatedCons;
}
}
js-meta.xml:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
To test, add the LWC to the Opportunity Lightning Record Page.