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

linkGetting 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

You will also need to download WebViewer.

linkSetting 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

linkOptimizing WebViewer Source Code for Salesforce

Now you need to optimize the original PDFTron WebViewer code for Salesforce. To accomplish this, extract the WebViewer.zip that you downloaded earlier into a folder, change your working directory to where the contents of WebViewer.zip were extracted to:

$ tree -L 1
.
├── doc
├── lib
├── licenses
├── package.json
├── samples
├── scripts
└── server.js

And run this npm script:

$ npm run optimize

You will encounter the following prompts. Answer y/n as indicated:

Optimize: Do you want us to backup your files before optimizing? [y/n]:  y
Optimize: Will you be using WebViewer Server? ... [y/n]:  n
Optimize: Will you be converting all your documents to XOD? ... [y/n]:  n
Optimize: Do you need client side office support? [y/n]:  y
Optimize: Do you need the full PDF API? ... [y/n]:  y
Optimize: Do you want to use the production version of PDFNet.js? ... [y/n]:  n
Optimize: Do you need to deploy to Salesforce? ... [y/n]:  y

Note: Make sure you answer this prompt with n:

Optimize: Will you be converting all your documents to XOD? ... [y/n]:  n

After answering y to Do you need to deploy to Salesforce?, the script will produce .zip files of no more than 5 mb in size, enabling you to upload them as static resources to Salesforce.

linkInstalling 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
  2. Copy all the zip files generated after running the npm run optimize script from the output folder webviewer-salesforce-attachments into the force-app/main/default/staticresources folder of your newly cloned project.
The files you will need to copy from the webviewer-salesforce directory

Figure: The files you will need to copy from the “webviewer-salesforce” directory.

  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})
  2. If you haven’t done so, authenticate your org and provide it with an alias (DevHub in the command below) from your terminal (Unix/macOS) or cmd (Windows). Execute following command as is.

    sfdx force:auth:web:login --setalias [your-alias] [--instanceurl https://test.salesforce.com for sandboxes]

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
  2. Create a scratch org using the config/project-scratch-def.json file, set the username as your default, and assign it an alias by replacing my-scratch-org with your own alias name. Alternatively, you can also deploy to your sandbox and skip this step.

    sfdx force:org:create --setdefaultusername -f config/project-scratch-def.json --setalias my-scratch-org
  3. Push the app to your org:

    sfdx force:source:push -f
  4. Open the org:

    sfdx force:org:open [-a your-alias]
  5. Navigate to the Object 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

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

salesforce-devhub-enable

linkImplementation Details for Developers

linkGetting 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:

<!-- pdftronWvRecordContainer.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__RecordPage</target>
    </targets>
</LightningComponentBundle>

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

import { LightningElement, api } from 'lwc';

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

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

<!--- pdftronWvContainer.html --->
<template>
        <lightning-card>
            <div id="cardContainer">
                <div id="comboboxContainer">
                    <c-attachment-picker-combobox record-id={recordId}>
                    </c-attachment-picker-combobox>
                </div>
                <div id="wvContainer">
                    <c-wv-instance record-id={recordId}></c-wv-instance>
                </div>
            </div>
        </lightning-card>
</template>

Once you have the record Id available in your target LWC, pass it to your Apex method using the @wire decorator.

import { LightningElement, track, wire, api } from 'lwc';
import getAttachments from "@salesforce/apex/PDFTron_ContentVersionController.getAttachments";
 
//shortened for brevity
 
export default class PdftronAttachmentPickerCombobox extends LightningElement {
 
   @track picklistOptions = [];
   @api recordId;
 
   @wire(getAttachments, {recordId: "$recordId"})
   attachments({error, data}) {
       if (data) {
           data.forEach((attachmentRecord) => {
               var name = attachmentRecord.cv.Title + "." + attachmentRecord.cv.FileExtension;
               const option = {
                   label: name,
                   value: JSON.stringify(attachmentRecord)
               };
               this.picklistOptions = [ ...this.picklistOptions, option ];
           });
           error = undefined;
       } else if (error) {
           console.error(error);
           this.error = error;
           this.picklistOptions = undefined;
       }
   };
}

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
List<String> cdIdList = new List<String> ();
List<ContentVersionWrapper> cvwList = new List<ContentVersionWrapper> ();

//Find links between record & document
for (ContentDocumentLink cdl :
        [ SELECT Id, ContentDocumentId
          FROM ContentDocumentLink
          WHERE LinkedEntityId = :recordId
        ]) {
    cdIdList.add(cdl.ContentDocumentId);
}
//Use links to get attachments
for (ContentVersion cv :
        [ SELECT Id, Title,FileExtension, VersionData
          FROM ContentVersion
          WHERE ContentDocumentId IN :cdIdList
          AND IsLatest = true
        ]) {
    if (fileFormats.contains(cv.FileExtension.toLowerCase())) {
        cvwList.add(new ContentVersionWrapper(cv));
    }
}

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);
    }
}

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

linkFile 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

linkHow 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);
}

linkSetting 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.

linkCommunicating with CoreControls from Lightning Web Component

linkAdditional Features

You can now start adding different features to your viewer:

linkWrap 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!