Jak skorzystać z Workspace API w LWC
Developowanie w Salesforsie to niejednokrotnie konieczność radzenia sobie z rozmaitymi ograniczeniami, które ten system na nas nakłada. W serwisie IdeaExchange już niemal dwa lata zbierane są głosy za implementacją aurowego odpowiednika Workspace API w LWC.
Workspace API, co to jest?
Workspace API to komponent dostarczający metody, za pomocą których możemy kontrolować taby i subtaby w Console Navigation w Salesforce Lightning Experience: otwierać, zamykać, zmieniać focus, ikony, nazwy i inne.
Jeśli wobec tego w projekcie dbamy o to, żeby korzystać z najnowszych dostępnych technologii i nasz front-end jest oparty na LWC, to w jaki sposób skorzystać ze wszystkich dobrodziejstw Workspace API?
Stwórzmy sobie niewielką userstory na potrzeby naszego artykułu – na stronie Accounta wyświetlamy kontakty i chcemy dać użytkownikowi możliwość otwarcia rekordu w nowym tabie albo subtabie.
Oczywiście komponent wyświetlający kontakty oraz przyciski jest napisany w LWC, zatem:
<template>
<lightning-card title=”Contact Records” icon-name=”standard:contact”>
<template if:true={contacts.data}>
<template for:each={contacts.data} for:item=”contact”>
<div key={contact.Id} class=”slds-m-top_small”>
<span class=”slds-p-right_small”>{contact.Name}</span>
<lightning-button-group>
<lightning-button label=”Otwórz w tabie”
data-id={contact.Id}
onclick={handleTabOpen}>
</lightning-button>
<lightning-button label=”Otwórz w subtabie”
data-id={contact.Id}
onclick={handleSubtabOpen}>
</lightning-button>
</lightning-button-group>
</div>
</template>
</template>
</lightning-card>
</template>
import { LightningElement, api, wire } from 'lwc’;
import getAccountsContacts from ’@salesforce/apex/AccountService.getAccountsContacts’;
export default class WorkspaceApiUser extends LightningElement {
@api recordId;
@wire(getAccountsContacts, { accountId: '$recordId’ }) contacts;
}
<?xml version=”1.0″ encoding=”UTF-8″?>
<LightningComponentBundle xmlns=”http://soap.sforce.com/2006/04/metadata”>
<apiVersion>55.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
Kod kontrolera apeksowego:
public with sharing class AccountService {
@AuraEnabled(cacheable=true)
public static List<Contact> getAccountsContacts(Id accountId){
return [
SELECT Id, Name FROM Contact WHERE AccountId = :accountId
];
}
}
Żeby przesłać wiadomość z LWC do komponentu aurowego, który będzie odpowiedzialny za operacje na Workspace API, skorzystamy z Lightning Message System.
Lightning Message System został wprowadzony w release’ie Summer 2020, umożliwia komunikację pomiędzy komponentami typu Visualforce, Aura i Lightning Web Components, umieszczonymi na wspólnej Lightning Page. Żeby użyć API LMS, utworzymy komponent typu Lightning Message Channels.
W tym celu w katalogu
force-app/main/default/messageChannels/
tworzymy plik LwcWorkspaceApi.messageChannel-meta.xml, w którym definiujemy pole methods, odpowiedzialne za przesyłanie danych: będziemy nim przesyłać obiekt zawierający nazwę metody oraz parametry wejściowe.
<?xml version=”1.0″ encoding=”UTF-8″?>
<LightningMessageChannel xmlns=”http://soap.sforce.com/2006/04/metadata”>
<masterLabel>ConnectAuraAndLWC</masterLabel>
<isExposed>true</isExposed>
<description>Send data from LWC to Workspace API</description>
<lightningMessageFields>
<fieldName>methods</fieldName>
<description>Methods to be run</description>
</lightningMessageFields>
</LightningMessageChannel>
Importujemy niezbędne elementy oraz sam kanał w naszym komponencie:
import { MessageContext, publish } from 'lightning/messageService’;
import lwcWorkspaceApi from '@salesforce/messageChannel/LwcWorkspaceApi__c’;
Oraz powiązujemy z messageContextem
@wire(MessageContext) messageContext;
Teraz możemy przejść do zaimplementowania metod, które będą wysyłać wiadomość za pomocą LMS
handleTabOpen(event) {
const contactId = event.target.dataset.id;
const operation = 'openTab’;
const parameters = {
recordId: contactId,
focus: true
};
this.sendMessage({ operation, parameters });
}
sendMessage(methods) {
publish(this.messageContext, lwcWorkspaceApi, { methods });
}
Następnie przejdziemy do utworzenia komponentu aurowego workspaceApiUtils. Jego zadaniem będzie przechwytywanie wiadomości LMS oraz wykonywanie operacji na Workspace API.
<aura:component implements=”flexipage:availableForRecordHome”>
<lightning:messageChannel type=”LwcWorkspaceApi__c”
onMessage=”{!c.handleMessageReceived}”
scope=”APPLICATION” />
<lightning:workspaceAPI aura:id=”workspace” />
<aura:attribute name=”methods” type=”Object” />
</aura:component>
({ // controller
handleMessageReceived: function (component, message, helper) {
const methods = message.getParam(’methods’);
component.set(’v.methods’, method);
helper.runMethod(component);
}
})
({ // helper
runMethod: function (component) {
const workspaceAPI = component.find(’workspace’);
const { operation, parameters } = component.get(’v.methods’)
workspaceAPI[operation](parameters)
.then(response => console.log(response))
.catch(error => console.error(error))
}
})
Zostało nam jeszcze umieścić nasze komponenty na Lightning Page’u i zobaczyć, czy wszystko działa, jak należy.
Przejdźmy teraz do kolejnego etapu – otwierania nowego subtaba. Tutaj sprawa robi się bardziej złożona, ponieważ jako parametr metody openSubtab należy podać Id nadrzędnego taba. Żeby uzyskać to Id, możemy użyć na przykład metody getEnclosingTab(). Wszystkie metody obsługujące taby i subtaby zwracają Obietnice (“Promise”), więc w czystej Aurze nasze rozwiązanie miałoby taki kształt:
const workspaceAPI = component.find(„workspace”);
workspaceAPI.getEnclosingTabId()
.then(tabId => workspaceAPI.openSubtab({
parentTabId: tabId,
recordId: '0037R00002tUyCWQA0′,
focus: true
}));
Jak to jednak powiązać z LWC i LMS? Odpytać WorkspaceAPI o Id taba, wysłać je LMSem z powrotem do LWC i tam przygotować kolejną wiadomość już z wypełnionym ParentTabId? Stopień skomplikowania takiego rozwiązania byłby zbyt duży, spróbujmy więc przystosować nasz LWC do wysyłania, a Aurę do przyjmowania nie jednego, ale listy obiektów z nazwą metody i parametrami, do tworzenia z niej listy Obietnic i wykonywania ich po kolei.
LWC:
handleSubtabOpen(event) {
const contactId = event.target.dataset.id;
const methods = [
{
operation: 'getEnclosingTabId’,
parameters: {}
},
{
operation: 'openSubtab’,
parameters: {
recordId: contactId,
parentTabId: '<RESPONSE>’,
focus: true
}
}
];
this.sendMessage(methods);
}
Wprowadzamy symbol zastępczy <RESPONSE> który już w Aurze podmienimy na właściwą wartość, którą otrzymamy jako odpowiedź na wywołanie getEnclosingTab.
Przebudowę aury zaczniemy od komponentu:
<aura:component implements=”flexipage:availableForRecordHome”>
<lightning:messageChannel type=”LwcWorkspaceApi__c”
onMessage=”{!c.handleMessageReceived}”
scope=”APPLICATION” />
<lightning:workspaceAPI aura:id=”workspace” />
<aura:attribute name=”methods” type=”Object[]” />
<aura:attribute name=”latestResponse” type=”String” />
</aura:component>
Atrybut methods będzie teraz tablicą obiektów, dodajemy też drugi atrybut – latestResponse, w którym będziemy przechowywać ostatnią zwróconą wartość. Teraz będziemy mogli ją użyć w kolejnej metodzie z Workspace API.
({ // controller
handleMessageReceived: function (component, message, helper) {
const methods = message.getParam(’methods’);
component.set(’v.methods’, methods);
const index = 0;
helper.runNextWorkspaceApiMethod(component, index);
}
})
Metoda runNextWorkspaceApiMethod będzie odpowiedzialna za pobranie listy wszystkich operacji i uruchomienia odpowiedniej pozycji, dlatego przy pierwszym uruchomieniu podajemy pierwszą (czyli zerową :).
A teraz helper:
({ // helper
runNextWorkspaceApiMethod: function (component, index) {
const that = this;
const methods = component.get(’v.methods’);
const operation = methods[index].operation;
const parameters = that.parseParameters(component, methods[index].parameters);
that.runMethod(component, operation, parameters)
.then(() => {
index++;
if (index < methods.length) {
that.runNextWorkspaceApiMethod(component, index);
}
})
},
parseParameters: function (component, parameters) {
const latestResponse = component.get(’v.latestResponse’);
const entries = Object.entries(parameters).map(([key, value]) => {
if (value === '<RESPONSE>’) {
return [key, latestResponse];
}
return [key, value]
});
return Object.fromEntries(entries);
},
runMethod: function (component, operation, parameters) {
const workspaceAPI = component.find(’workspace’);
const promise = new Promise((resolve, reject) => {
workspaceAPI[operation](parameters)
.then(response => {
component.set(’v.latestResponse’, response);
resolve(response)
})
.catch(error => reject(error))
});
return promise;
}
})
Metoda runMethod tworzy Promisę, która w przypadku rozwiązania zapamiętuje wynik w atrybucie latestResponse, a w przypadku odrzucenia – zwraca błąd.
Wywołująca ją runNextWorkspaceApiMethod – jeśli otrzyma rozwiązaną Promisę – sprawdza, czy na liście operacji są jeszcze kolejne do wykonania, a jeśli tak, to wywołuje samą siebie ze zwiększonym o jeden indeksem.
Metoda parseParameters dba o to, żeby w przypadku obecności symbolu zastępczego <RESPONSE> został on zastąpiony przez ostatnio otrzymaną odpowiedź, którą mamy zapisaną w atrybucie latestResponse.
Sprawdzamy. Działa.
Pozostała jeszcze jedna rzecz do uwzględnienia w naszym rozwiązaniu – niektóre z metod Workspace API zwracają Promisy rozwiązujace się nie do pojedynczej wartości, tylko do całych obiektów. Jak w takim przypadku dostać się do tej jednej, konkretnej wartości, której potrzebujemy?
Rozszerzmy nieco nasze userstory – chcemy po otwarciu subtaba zmienić mu nazwę (“Nowy subtab”) i zmienić domyślną ikonę (na “action:approval”).
LWC:
handleSubtabOpen(event) {
const contactId = event.target.dataset.id;
const methods = [
{
operation: 'getEnclosingTabId’,
parameters: {}
},
{
operation: 'openSubtab’,
parameters: {
recordId: contactId,
parentTabId: '<RESPONSE>’,
focus: true
}
},
{
operation: 'setTabLabel’,
parameters: {
tabId: '<RESPONSE>’,
label: 'Nowy Subtab’
}
}, {
operation: 'setTabIcon’,
parameters: {
tabId: '<RESPONSE>.tabId’,
icon: 'action:approval’
}
}
];
this.sendMessage(methods);
}
W Aurze rozbudujemy metodę parseParameters – będzie ona sprawdzać, czy podaliśmy po kropce pole obiektu, którego wartość chcemy użyć.
parseParameters: function (component, parameters) {
const latestResponse = component.get(’v.latestResponse’);
const entries = Object.entries(parameters).map(([key, value]) => {
if (/<RESPONSE>/g.test(value)) {
const dotPosition = value.indexOf(’.’);
if (dotPosition >= 0) {
const responseField = value.substring(dotPosition + 1);
return [key, latestResponse[responseField]];
}
return [key, latestResponse];
}
return [key, value]
});
return Object.fromEntries(entries);
},
Efekt? Taki, jakiego oczekiwaliśmy:
Całość znajdziecie na Githubie
Autor
Maciej Król
Salesforce Developer
Masz pomysł na wpis? Chciałbyś podzielić się swoją wiedzą i doświadczeniem? Współtwórz portal razem z nami. Nie czekaj, napisz już dziś na adres redakcji: redakcja@coffeeforce.pl , a my się z Tobą skontaktujemy.