Hi Everyone,
In this post, I am going to show you how to download and Preview the files from Lightning Community using Lightning Web Component.
Before we get started, let’s discuss the Object Structure of the files to know how the files get stored into Salesforce.
Content Document: – Represents a document that has been uploaded to a library in Salesforce CRM Content or Salesforce Files.
Content Version: – Represents a specific version of a document in Salesforce CRM Content or Salesforce Files. One Content Document can have multiple content versions.
Content Document Link:- Represents the link between a Salesforce CRM Content document or Salesforce file and where it’s shared. A file can be shared with other users, groups, records, and Salesforce CRM Content libraries.
Here is the complete diagram.
This is how your component will look like once you have completed the development of the component.
Features
- Preview the Files in both Lightning Experience and Lighting Community
- Download the Files in both Lightning Experience and Lighting Community
- Delete the files
- Upload new files under the same record
- Filter the files based on created date and file title
- Sync the files from Salesforce if the file has been uploaded from a different place.
- Control what information needs to show in the table.
Limitations
- The only limitation of the component is, we can use it under the record detail page only.
Use Component inside Salesforce Record Page in Lightning Experience
To use the component, Open the record detail page of any object where you wanted to use this component. Click on gear icon and then click on Edit page.
In the left, search for content manager, drag and drop the component where you wanted to place.
It requires some additional information that your salesforce admin can setup. See below screenshot
Setup Component in Lighting Experience
- Title: – Title of the Lighting Card
- Record Id: – use recordId as value for Lighting Experience and {!recordId} if component is used in Lightning Community
- Do you want to show Details? – to display the file details. Keep as true
- Accepted file format to upload the files? – It has some default values. if you wanted to remove or add make the adjustment.
- User Can Sync the files from Salesforce? – If true it will show a button to sync the files from salesforce.
- Do you want the users to upload the new files? – If true, the user will be able to see the upload button.
- The component is used in the community? – Check this as true if the component is being used in the Salesforce Community.
- Show Filters? – If true, the user will be able to see the filters.
Let’s setup the code.
Create the Apex Class.
/**
* @description :
* @author : Amit Singh
* @group :
* @last modified on : 12-02-2020
* @last modified by : Amit Singh
* Modifications Log
* Ver Date Author Modification
* 1.0 11-26-2020 Amit Singh Initial Version
**/
public with sharing class ContentManagerService {
@AuraEnabled
public static String getContentDetails(String recordId) {
List<ContentDocumentLink> contentDocumentList = [SELECT ContentDocumentId, LinkedEntityId
FROM ContentDocumentLink
WHERE LinkedEntityId =: recordId];
Set<Id> contentDocumentId = new Set<Id>();
for(ContentDocumentLink cdl : contentDocumentList){
contentDocumentId.add(cdl.ContentDocumentId);
}
List<ContentVersion> contentVersionList = [SELECT Id, VersionData, FileType, Title, FileExtension,
ContentDocument.CreatedBy.Name, ContentDocument.ContentSize,
CreatedDate, ContentDocumentId, ContentDocument.FileType
FROM ContentVersion
WHERE ContentDocumentId IN : contentDocumentId];
return JSON.serialize(contentVersionList);
}
@AuraEnabled
public static void deleteContentDocument(String recordId) {
Database.delete(recordId);
}
public static string ContentType(String fileType) {
switch on fileType.toLowerCase(){
when 'docx' {
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
}
when 'csv' {
return 'application/vnd.ms-excel';
}
when 'wav' {
return 'audio/wav';
}
when 'wmv' {
return 'video/x-ms-wmv';
}
when 'mp3' {
return 'audio/mpeg';
}
when 'mp4' {
return 'video/mp4';
}
when 'png' {
return 'image/png';
}
when 'pdf' {
return 'application/pdf';
}
when else {
return 'image/jpeg';
}
}
}
}
Create the Lightning Web Component and use below code.
Code for HTML file
<!--
@description :
@author : Amit Singh
@group :
@last modified on : 12-02-2020
@last modified by : Amit Singh
Modifications Log
Ver Date Author Modification
1.0 11-26-2020 Amit Singh Initial Version
-->
<template>
<lightning-card variant="Narrow" title={title} icon-name="standard:document">
<template if:true={isLoading}>
<lightning-spinner alternative-text="Loading" size="small" variant="brand"></lightning-spinner>
</template>
<lightning-button if:true={showsync} variant="brand" title="Content Sync" icon-name="utility:sync"
onclick={handleSync} slot="actions" >
</lightning-button>
<div class="slds-var-p-around_small" >
<div class="slds-grid slds-wrap">
<div class="slds-col slds-size_1-of-4" if:true={showFilters}>
<lightning-input if:true={showFilters} type="search" variant="standard" name="Title" label="Title" onchange={handleSearch} >
</lightning-input>
</div>
<div class="slds-col slds-size_1-of-4" if:true={showFilters}>
<lightning-input if:true={showFilters} type="search" variant="standard" name="Created By" label="Created By" onchange={handleSearch}>\
</lightning-input>
</div>
<div class="slds-col slds-size_2-of-4 slds-var-p-left_small">
<lightning-file-upload if:true={showFileUpload}
label="Upload New File"
name="fileUploader"
accept={accept}
record-id={recordId}
onuploadfinished={handleUploadFinished}
multiple>
</lightning-file-upload>
</div>
</div>
</div>
<div class="slds-var-p-around_small">
<lightning-datatable
key-field="id"
data={dataList}
hide-checkbox-column
columns={columnsList}
onrowaction={handleRowAction}>
</lightning-datatable>
</div>
</lightning-card>
</template>
Code for javascript files.
import { api, LightningElement, track, wire } from 'lwc';
import getContentDetails from '@salesforce/apex/ContentManagerService.getContentDetails';
import deleteContentDocument from '@salesforce/apex/ContentManagerService.deleteContentDocument';
import { NavigationMixin } from 'lightning/navigation';
const columns = [
{ label: 'Title', fieldName: 'Title', wrapText : true,
cellAttributes: {
iconName: { fieldName: 'icon' }, iconPosition: 'left'
}
},
{ label: 'Created By', fieldName: 'CREATED_BY',
cellAttributes: {
iconName: 'standard:user', iconPosition: 'left'
}
},
{ label: 'File Size', fieldName: 'Size' },
{ label: 'Preview', type: 'button', typeAttributes: {
label: 'Preview', name: 'Preview', variant: 'brand-outline',
iconName: 'utility:preview', iconPosition: 'right'
}
},
{ label: 'Download', type: 'button', typeAttributes: {
label: 'Download', name: 'Download', variant: 'brand', iconName: 'action:download',
iconPosition: 'right'
}
},
{ label: 'Delete', type: 'button', typeAttributes: {
label: 'Delete', name: 'Delete', variant: 'destructive',iconName: 'standard:record_delete',
iconPosition: 'right'
}
}
];
export default class ContentManager extends NavigationMixin(LightningElement) {
@api title;
@api showDetails;
@api showFileUpload;
@api showsync;
@api recordId;
@api usedInCommunity;
@api showFilters;
@api accept = '.csv,.doc,.xsl,.pdf,.png,.jpg,.jpeg,.docx,.doc';
@track dataList;
@track columnsList = columns;
isLoading = false;
wiredFilesResult;
connectedCallback() {
this.handleSync();
}
getBaseUrl(){
let baseUrl = 'https://'+location.host+'/';
return baseUrl;
}
handleRowAction(event){
const actionName = event.detail.action.name;
const row = event.detail.row;
switch (actionName) {
case 'Preview':
this.previewFile(row);
break;
case 'Download':
this.downloadFile(row);
break;
case 'Delete':
this.handleDeleteFiles(row);
break;
default:
}
}
previewFile(file){
if(!this.usedInCommunity){
this[NavigationMixin.Navigate]({
type: 'standard__namedPage',
attributes: {
pageName: 'filePreview'
},
state : {
selectedRecordId: file.ContentDocumentId
}
});
} else if(this.usedInCommunity){
this[NavigationMixin.Navigate]({
type: 'standard__webPage',
attributes: {
url: file.fileUrl
}
}, false );
}
}
downloadFile(file){
this[NavigationMixin.Navigate]({
type: 'standard__webPage',
attributes: {
url: file.downloadUrl
}
}, false
);
}
handleDeleteFiles(row){
this.isLoading = true;
deleteContentDocument({
recordId : row.ContentDocumentId
})
.then(result => {
this.dataList = this.dataList.filter(item => {
return item.ContentDocumentId !== row.ContentDocumentId ;
});
})
.catch(error => {
console.error('**** error **** \n ',error)
})
.finally(()=>{
this.isLoading = false;
});
}
handleSync(){
let imageExtensions = ['png','jpg','gif'];
let supportedIconExtensions = ['ai','attachment','audio','box_notes','csv','eps','excel','exe',
'flash','folder','gdoc','gdocs','gform','gpres','gsheet','html','image','keynote','library_folder',
'link','mp4','overlay','pack','pages','pdf','ppt','psd','quip_doc','quip_sheet','quip_slide',
'rtf','slide','stypi','txt','unknown','video','visio','webex','word','xml','zip'];
this.isLoading = true;
getContentDetails({
recordId : this.recordId
})
.then(result => {
let parsedData = JSON.parse(result);
let stringifiedData = JSON.stringify(parsedData);
let finalData = JSON.parse(stringifiedData);
let baseUrl = this.getBaseUrl();
finalData.forEach(file => {
file.downloadUrl = baseUrl+'sfc/servlet.shepherd/document/download/'+file.ContentDocumentId;
file.fileUrl = baseUrl+'sfc/servlet.shepherd/version/renditionDownload?rendition=THUMB720BY480&versionId='+file.Id;
file.CREATED_BY = file.ContentDocument.CreatedBy.Name;
file.Size = this.formatBytes(file.ContentDocument.ContentSize, 2);
let fileType = file.ContentDocument.FileType.toLowerCase();
if(imageExtensions.includes(fileType)){
file.icon = 'doctype:image';
}else{
if(supportedIconExtensions.includes(fileType)){
file.icon = 'doctype:' + fileType;
}
}
});
this.dataList = finalData;
})
.catch(error => {
console.error('**** error **** \n ',error)
})
.finally(()=>{
this.isLoading = false;
});
}
handleUploadFinished(){
this.handleSync();
//eval("$A.get('e.force:refreshView').fire();");
}
formatBytes(bytes,decimals) {
if(bytes == 0) return '0 Bytes';
var k = 1024,
dm = decimals || 2,
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
handleSearch(event){
let value = event.target.value;
let name = event.target.name;
if( name === 'Title' ){
this.dataList = this.dataList.filter( file => {
return file.Title.toLowerCase().includes(value.toLowerCase());
});
} else if( name === 'Created By' ){
this.dataList = this.dataList.filter( file => {
return file.CREATED_BY.toLowerCase().includes(value.toLowerCase());
});
}
}
}
Code for meta.xml file
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>50.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Content Manager</masterLabel>
<targets>
<target>lightning__RecordPage</target>
<target>lightning__UtilityBar</target>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
<!-- Configuring the design attributes -->
<targetConfigs>
<targetConfig targets="lightning__RecordPage, lightningCommunity__Default">
<property name="title" type="String" label="Title" default="Content Manager" required="true"/>
<property name="recordId" type="String" default="recordId" label="Record Id" required="true"/>
<property name="showDetails" type="Boolean" default="true" label="Do you want to Show Details ?"/>
<property name="accept" type="String" default=".csv,.doc,.xsl,.pdf,.png,.jpg,.jpeg,.docx,.doc"
label="User can upload the files in format?" />
<property name="showsync" type="Boolean" default="true" label="User can sync the files from Salesforce?" />
<property name="showFileUpload" type="Boolean" default="true" label="Do you want the users to upload a new file ?"/>
<property name="usedInCommunity" type="Boolean" default="false" label="Component is used in Community?" />
<property name="showFilters" type="Boolean" default="true" label="Show Filters?" />
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Deploy the code to your salesforce org and Enjoy.
Thanks for reading 🙂
#HappyReading #DeveloperGeeks #AskPanther #SFDCPanther
Hi Amit,
I am new to SF, couple of weeks in. So far your article is the best example out there.
I need to get hold of downloaded binary/image similar to standard ajax request below
$.ajax({
url: url,
type: “GET”,
dataType: ‘binary’,
headers: header,
processData: false,
success: function (result) {
var data = new Blob([result], ['']);
}
});
However, I need to use NavigationMixin.Navigate with
file.downloadUrl = baseUrl+’sfc/servlet.shepherd/document/download/’+file.ContentDocumentId;
as my files will most likely exceed the memory limits.
Thanks
Hi,
I understand that there is a limit of 6 MB and I am still looking for a way where we can find some other ways to download the files.
Hi Amit,
I tried this code, when I upload the CSV file and click on the Preview button it’s giving me the error “URL No Longer Exists”. what can be the reason for this?
Is there any possibilities that the file has been deleted behind the scene?
The download option doesn’t work as shown in the code here for Communities. The assets aren’t stored in the same Salesforce path when accessing the document ID for download in a community.
I created a separate variable for “Community Download URL” with the value: this.communityURL+’/sfc/servlet.shepherd/document/download/’+file.ContentDocumentId;
where “this.communityURL” is the fully qualified path of your Community. This could differ from your basepath.
Additionally, when downloading, I have a ternary operator to decide which download URL to use when the button is clicked:
url: this.usedInCommunity? file.communityDownloadURL : file.internalDownloadURL
In case that helps someone else
Thanks for your addition.
Hi Amit,
I have tried this code and deployed it successfully but it is not working, could see only the doc icon and header titles. I am unable to see the buttons and search boxes. If you don’t mind help me on this to fix this issue or do we have a video on our youtube channel.
Thanks
You need to provide ContentManagerService class access to the correct profile
already ContentManagerService class access is enabled to the system admin profile.
Check if you have configured the component in a correct way.