At PDFTron, many clients approached us looking for an end-to-end document solution for their Salesforce applications -- one that could handle their most commonly used document file types and integrate seamlessly within their existing workflows.

Well, we've listened. Now you can view, edit, and annotate all your attached record files, including MS Office documents, right in a Salesforce application, using our expanded WebViewer integration. This article shows you how to easily integrate, customize, and extend such a solution.

salesforce-devhub-enable

Getting Started

To get started with WebViewer and Salesforce, first clone our sample Lightning Web Component project from GitHub.

# Via HTTPS
git clone https://github.com/PDFTron/salesforce-webviewer-attachments.git
# Via SSH
git clone git@github.com:PDFTron/salesforce-webviewer-attachments.git

Setting up Salesforce DX

We recommend using Salesforce DX when deploying this application to your sandbox or scratch org. If you are unfamiliar with the tool, we recommend the Quick Start: Salesforce DX Trailhead Project or the App Development with Salesforce DX Trailhead module. Important steps include:

salesforce-devhub-enable

In this guide we are using the SFDX CLI so here are some common parameters we will be using:

-a // sets the given alias name
-f // find a file using file path
-r // go to given url
-s // sets the scratch org as default org

Installing the Sample LWC App using Salesforce DX

Next, you need to clone our sample webviewer-salesforce-attachments project, configure it accordingly, and get it up and running. Follow these steps:

  1. Clone the salesforce-webviewer-attachments GitHub repo:
# Via HTTPS
git clone https://github.com/PDFTron/salesforce-webviewer-attachments.git
# Via SSH
git clone git@github.com:PDFTron/salesforce-webviewer-attachments.git
cd webviewer-salesforce-attachments
  1. You can add your WebViewer license key in staticresources/myfiles/config.js file or add it in WebViewer constructor by passing l: "LICENSE_KEY" option.
WebViewer({
  l:LICENSE_KEY})
  1. If you haven’t done so, authenticate your org and provide it with an alias assigning DevHub as an alias reference and a url to direct to the org login page. Execute the following command as is from your terminal (Unix/macOS) or cmd (Windows).
sfdx force:auth:web:login -a DevHub -r https://login.salesforce.com

Alternatively, you can authenticate from VS Code: salesforce-devhub-enable

  1. Enter your org credentials in the browser that opens (for sandboxes, you can advance to step 7). If you would like to use a scratch org, open Settings and type dev hub in the quick find search. Make sure the toggle is set to enabled as shown in the picture below.

salesforce-devhub-enable

  1. Create a scratch org using the config/project-scratch-def.json file, sets the scratch org as default org, and assign it an alias [alias name]. Alternatively, you can also deploy to your sandbox and skip this step.
sfdx force:org:create -s -f config/project-scratch-def.json -a [alias-name]
  1. Push the app to your org:
sfdx force:source:push
  1. Enter some dummy data:
sfdx force:data:record:create -s Account -v "Name='Marriott Marquis' BillingStreet='780 Mission St' BillingCity='San Francisco' BillingState='CA' BillingPostalCode='94103' Phone='(415) 896-1600' Website='www.marriott.com'"
  1. Open the org:
sfdx force:org:open -u [alias name]
  1. Navigate to the dummy data that you would like to use for editing your record files. Click on the gear wheel and select ‘Edit Page’, which opens the Lightning App Builder.

salesforce-devhub-enable

  1. Now select where you would like to launch the Lightning Web Component from and drag and drop the pdftronWebviewerContainer component there.

salesforce-devhub-enableFigure: Other pdftron LWC make up pdftronWebviewerContainer

Implementation Details for Developers

Getting the attachments from the current record

To send the current record Id to your Lightning Web Component, include the following: <target>lightning__RecordPage</target> in your js-meta.xml file like so:

<!-- pdftronWebviewerContainer.js-meta.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>50.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
      <target>lightning__AppPage</target>
      <target>lightning__RecordPage</target>
      <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

Now you can use the record Id in your JS like so:

import { LightningElement, api } from 'lwc';

export default class PdftronWebviewerContainer extends LightningElement {
    @api recordId;
}

You can also pass the record Id to any child components:

<!--- pdftronWebviewerContainer.html --->
<template>
    <lightning-card>
        <div id="cardContainer" class="slds-p-around_x-small">
            <div id="comboboxContainer">
                <c-pdftron-attachment-picker-combobox record-id={recordId}>
                </c-pdftron-attachment-picker-combobox>
            </div>
            <div id="wvContainer">
                <c-pdftron-wv-instance record-id={recordId}></c-pdftron-wv-instance>
            </div>
        </div>
    </lightning-card>
</template>

We'll be retrieving the attachment imperatively:

//shortened for brevity
import { LightningElement, track, wire, api } from 'lwc';
import getAttachments from '@salesforce/apex/PDFTron_ContentVersionController.getExistingAttachments'
 

export default class PdftronAttachmentPickerCombobox extends LightningElement {
   @api recordId;
   @track attachments = []
 
   getAttachments({ recordId: this.recordId })
        .then(data => {
          this.attachments = data
          this.initLookupDefaultResults()

          this.error = undefined
          this.loadFinished = true
          this.documentsRetrieved = true
        })
        .catch(error => {
          console.error(error)
          this.showNotification('Error', error, 'error')
          this.error = error
        })
}

Next, in your Apex Controller, you can use the following query to get the records' ContentDocumentLink record, which in turn, can be used to gather the ContentVersion records like so:

// PDFTron_ContentVersionController.cls - shortened for brevity
@AuraEnabled
public static List<ContentVersionWrapper> getAttachments(String recordId){
    try {
        List<String> cdIdList = new List<String> ();
        List<ContentVersionWrapper> cvwList = new List<ContentVersionWrapper> ();

        //Find links between record & document
        for(ContentDocumentLink cdl : 
                [   SELECT id, ContentDocumentId, ContentDocument.LatestPublishedVersionId 
                    FROM ContentDocumentLink 
                    WHERE LinkedEntityId = :recordId    ]) {
            cdIdList.add(cdl.ContentDocumentId);
        }
        //Use links to get attachments
        for(ContentVersion cv : 
                [   SELECT Id, Title,FileExtension, VersionData, ContentDocumentId 
                    FROM ContentVersion 
                    WHERE ContentDocumentId IN :cdIdList 
                    AND IsLatest = true ]) {
            if(checkFileExtension(cv.FileExtension)) {
                cvwList.add(new ContentVersionWrapper(cv, false));
            }
        }
        return cvwList;
    } catch (Exception e) {
        throw new AuraHandledException(e.getMessage());
    }
}

You can include a wrapper class to pass relevant data to your LWC like so:

// shortened for brevity
public class ContentVersionWrapper {
    @AuraEnabled
    public String name {get; set;}
    @AuraEnabled
    public String body {get; set;}

    public ContentVersionWrapper(ContentVersion contentVer) {
        this.name = contentVer.Title + '.' + contentVer.FileExtension;
        this.body = EncodingUtil.base64Encode(contentVer.VersionData);
    }
}
@AuraEnabled
public static ContentVersionWrapper getBase64FromCv(String recordId) {
    try {
        ContentVersion cv = [SELECT Id, Title,FileExtension, VersionData FROM ContentVersion WHERE Id = :recordId AND IsLatest = true LIMIT 1];
        return new ContentVersionWrapper(cv, true);
    } catch (Exception e) {
        throw new AuraHandledException(e.getMessage());
    }
}

Check out the following guides to learn more about how to enable communication between your Lightning Web Component and WebViewer.

File size limitations

Salesforce’s multi-tenant architecture provisions each org with governor limits to ensure resources are shared appropriately. To maximize the effectiveness of your workflow, you should factor in these limits when designing your solution.

You may run into these limits depending on your workflow when using the client version of WebViewer. The most common limiting factors include the maximum heap size limit of 6MB(sync)/12MB(async) which are often exceeded when loading files larger than 30-50MB.

Also, make sure your organization meets your storage needs according to the data storage allocations: salesforce-devhub-enable

How to minimize heap usage

To allow for larger file processing, you need to ensure your allocated heap memory capacity will not get exceeded. Check this link to look at a few ways of doing this recommended by Salesforce.

The sample repository uses these guidelines to ensure heap memory is used effectively. An example includes iterating through your SOQL queries like so:

// Find links between record & document
for (ContentDocumentLink cdl :
        [ SELECT id, ContentDocumentId, ContentDocument.LatestPublishedVersionId
          FROM ContentDocumentLink
          WHERE LinkedEntityId = :recordId
        ]) {
    cdIdList.add(cdl.ContentDocumentId);
}

Setting Worker Paths in Config.js

Optimizing the original WebViewer source code for the Salesforce platform means that we will also have to set a few paths in config.js in order for WebViewer to function properly.

Communicating with CoreControls from Lightning Web Component

Additional Features

You can now start adding different features to your viewer:

Wrap up

To learn more about this integration, visit our Salesforce documentation section or watch our previously recorded webinar on integrating WebViewer into Salesforce . If you have any questions about implementing WebViewer in Salesforce, please feel free to get in touch and we will be happy to help!