Intro
3B Docs allows you to pull Salesforce files and then embed them on a document.
The File Object Structure
To merge a file (e.g. image) on a document, you will likely have the file(s) stored in the Files section against a record. This means that the documents in Salesforce are stored in a Document Link - Document - Content Version object structure.
- The Document Link - this links a salesforce record to a Content Document
- The Content Document - this is the metadata related to an uploaded document
- The Content Version - this is the actual data of the uploaded document
This guide does not discuss how to handle files stored as attachments, however a similar approach to the one described here can be employed
The Template
For the purposes of this guide, we will assume that we have a Contact record "Joe Bloggs" with an image of their passport uploaded as a File. We will assume that Joe Bloggs does not have any other files and we want to create a 3B template that embeds the passport image in the template.
- Create a Template, assuming the Contact is the Context object
- Select "Content Document Link" child merge object and copy the repeat block
- Paste the repeat block in the document
- Use the field selector to traverse up from Content Document Link to ContentDocument.LatestPublishedVersion.Id. This will give us the Latest Content Version Id
- Apply value formatter to pull the file based on the Content Version Id
The Filter
You can also add URL filter to the Content Document Link repeater in order to select a specific file to pull from the contextual record. Simply add the SOQL filter ContentDocument.Id in ({params.documents}) and pass the url param like so ?templateId=TEMPLATE_ID&recordId=RECORD_ID&documents=DOCUMENT_ID_1, DOCUMENT_ID_2.
Value Formatters
The value formatter is how the magic works. It is responsible for taking the content version's data by the content document Id and construct an <embed> or <img> element that will be returned in the document. There are a few ways to achieve this, and the example code below should be adapted to your specific needs. Simply create a Value Formatter and add the code from the examples below. You can name the Value Formatter something like "VersionDataToImage"
Option 1 - using sforce
Using sforce (globally available variable), you can create a value formatter as follows:
async (args) => {
console.log('+++++', args, sforce)
const getFile = function({versionId}){
return new Promise(async (resolve, reject) => {
const results = sforce.connection.query(`SELECT Id, VersionData, FileType, PathOnClient FROM ContentVersion WHERE Id = '${versionId}' LIMIT 1`);
console.info('sforce res', results)
resolve(results?.records);
});
}
const b64DecodeUnicode = function(str) {
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
const record = await getFile({versionId: args.value}).catch(err => {
console.error('value formatter error', err);
});
if(!record){
return 'Missing File';
}else if(!['jpg', 'png', 'jpeg'].includes(record.FileType.toLowerCase())){
return 'Unsupported file format: ' + record.FileType;
}
return `
<img src="data:image/png;base64,${record.VersionData}" width="100%" height="100%">
`
}
Assuming that the value formatter is called VersionDataToImage, here's an example on how to apply the formatter correctly:
#{ContentDocumentLink.Id:ContentDocument.LatestPublishedVersion.Id>VersionDataToImage}
The above assumes that the context record has a child ContentDocumentLink (all non-setup objects have that connection).
Option 2 - using REST API
Using rest API calls, you can include the sessionId from the window.globals.sessionId param like so
async (args) => {
const image = await fetch('/services/data/v46.0/sobjects/ContentVersion/0686e00000eQWhlAAG',{
headers: {
'Content-Type': 'application/octet-stream',
'Authorization': `Bearer ${globals.sessionId}`}
}
)
console.log('Img', image);
...
return '';
}
Option 3 - using RemoteRecordsManager
Using args.callout to call ApexClass (e.g. using the built in Records Management controller)
await (args) => {
args.callout('b3d.RemoteRecordsManager', {
endp: "getRecords",
objectName: 'ContentVersion',
getAllFields: false,
additionalFields: ['VersionData'],
queryFilter: `Id = '${args.value}'`
}).then(response => {
console.log('RemoteRecordsManager getRecords resp', response)
}).catch(err => {
console.error('RemoteRecordsManager getRecords error', err)
})
////....
}