Isn’t it painful if you have multiple Attachments on a record and you have multiple such records and you need to download all attachments/Documents?
Clicking every record and then opening each attachment and clicking download is a long and fussy process.
This component is a dual component and it will work 2-way.
- Download mass Attachments of a particular record by adding component on the record lightning page.
- Bulk download Attachments of any Object by adding component on the non-record page.
Component Features:
- Download attachments of any object.
- Download attachments of a particular record only.
- Pagination buttons to navigate.
- Set custom record display size.
- Filter attachments by Year/Month of any object.
Demo GIF:
- Record Lightning Page LWC Component
2. Home Lightning Page LWC Component
Screenshots:
Step-1: Create an Apex controller
Our apex controller will fetch the Object list if the component is added on the home lightning page and query the ContentDocument Records. Here we are using a wrapper class for object list which will return records in the form of value and label.
Create MassFilesDownloadController.cls Apex class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
/* Code by CafeForce || www.cafeforce.com || support@cafeforce.com || Mandatory Header */ public with sharing class MassFilesDownloadController { @AuraEnabled public static List<PicklistOptions> fetchObjectList() { List<PicklistOptions> objectList = new List<PicklistOptions>(); for(Schema.SObjectType objTyp : Schema.getGlobalDescribe().Values()) { Schema.DescribeSObjectResult describeSObjectRes = objTyp.getDescribe(); if(describeSObjectRes.isQueryable() && describeSObjectRes.isUpdateable() && describeSObjectRes.isSearchable() && describeSObjectRes.isAccessible() && describeSObjectRes.isCreateable() && !describeSObjectRes.isCustomSetting()) { String name = objTyp.getDescribe().getName(); if(!name.containsignorecase('history') && !name.containsignorecase('tag')&& !name.containsignorecase('share') && !name.containsignorecase('feed')) { objectList.add( new PicklistOptions(describeSObjectRes.getLabel(), describeSObjectRes.getName()) ); } } } objectList.sort(); return objectList; } @AuraEnabled public static List<ContentDocumentLink> fetchFiles(String objectName, String recordId, String year, String month) { try { String query = 'SELECT ContentDocumentId, ContentDocument.Title, ContentDocument.FileType, ContentDocument.ContentSize, ContentDocument.LastModifiedDate, ContentDocument.CreatedDate, LinkedEntity.Type FROM ContentDocumentLink '; if(String.isNotBlank(objectName)) { query += ' where LinkedEntityId in ( SELECT Id FROM '+objectName+') and LinkedEntity.Type =: objectName'; if(String.isNotBlank(year)) { query += ' AND calendar_year(ContentDocument.LastModifiedDate) = ' + year; } if(String.isNotBlank(month)) { query += ' AND calendar_month(ContentDocument.LastModifiedDate) = '+month; } } if(String.isNotBlank(recordId)) { query += ' where LinkedEntityId = \'' + recordId + '\''; } return Database.query(query); } catch(Exception ex) { throw new AuraHandledException(ex.getMessage()); } } public class PicklistOptions implements Comparable { @AuraEnabled public String label; @AuraEnabled public String value; public PicklistOptions(String label, String value) { this.label = label; this.value = value; } public Integer compareTo(Object ObjToCompare) { return label.CompareTo(((PicklistOptions)ObjToCompare).label); } } } /* Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE */ |
Step-2: Now we will create the massFilesDownload Lightning Web Component.
For creating a new LWC component, you need to use the VS Code IDE.
Open the HTML file massFilesDownload.html and paste the below code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
<!-- Code by CafeForce || www.cafeforce.com || support@cafeforce.com || Mandatory Header --> <template> <div class="MFD"> <div if:true={showSpinner}> <lightning-spinner alternative-text="Loading" size="small" ></lightning-spinner> </div> <div class="slds-clearfix slds-var-p-around_medium headerTopDiv"> <div class="slds-float_left"> <p class="slds-text-heading_small slds-p-around_x-small"><b>Mass Files Download <template if:true={data}> | Total Files - {totalFiles}</template> </b></p> </div> <div class="slds-float_right"> <lightning-button-group> <lightning-button-icon icon-name="utility:refresh" variant="border-filled" alternative-text="Refresh" onclick={fetchDocumentRecords} disabled={disableRecordDropdown}></lightning-button-icon> <lightning-button label="Download" icon-name="utility:download" onclick={downloadFiles}></lightning-button> <lightning-button-icon icon-name="utility:chevronleft" variant="border-filled" name="Previous" alternative-text="previous" onclick={handleNavigation} disabled={disablePreviousButton}></lightning-button-icon> <lightning-button-icon icon-name="utility:chevronright" variant="border-filled" name="Next" alternative-text="next" onclick={handleNavigation} disabled={disableNextButton}></lightning-button-icon> <lightning-combobox name="recordSize" variant="label-hidden" value={recordSize} placeholder="Page" options={recordSizeList} disabled={disableRecordDropdown} class="pageCombobox" onchange={handleRecordSizeChange} ></lightning-combobox> </lightning-button-group> </div> </div> <div if:false={recordId} class="slds-p-bottom_medium slds-p-left_medium slds-p-right_medium headerTopDiv"> <div class="displayInline"> <lightning-combobox name="object" variant="label-hidden" value={selectedbject} placeholder="Select Object" options={objectOptions} onchange={handleObjectChange} required></lightning-combobox> <lightning-combobox name="year" variant="label-hidden" value={selectedYear} placeholder="Year" options={yearOptions} class="yearCombobox" onchange={handleYearChange} ></lightning-combobox> <lightning-combobox name="month" variant="label-hidden" value={selectedMonth} placeholder="Month" options={monthOptions} class="monthCombobox" onchange={handleMonthChange} ></lightning-combobox> <lightning-button label="Fetch" variant="label-hidden" name="Fetch" onclick={fetchDocumentRecords} disabled={disableFetch}></lightning-button> </div> </div> <div class="bodyDiv"> <template if:true={recordList}> <div class="slds-grid slds-wrap tableHeader"> <div class="slds-col slds-size_1-of-12 slds-p-around_xx-small"> </div> <div class="slds-col slds-size_4-of-12 slds-p-top_x-small slds-p-bottom_x-small slds-p-right_x-small slds-p-left_small displayInline"> <lightning-input type="checkbox" data-id="headerCheckbox" onclick={handleHeaderCheckbox} name="headerCheckbox" variant="label-hidden"></lightning-input> <span class="slds-p-left_small">Title</span> </div> <div class="slds-col slds-size_2-of-12 slds-p-around_x-small"> Size </div> <div class="slds-col slds-size_2-of-12 slds-p-around_x-small"> Type </div> <div class="slds-col slds-size_2-of-12 slds-p-around_x-small"> Last Modified </div> <div class="slds-col slds-size_1-of-12 slds-p-around_x-small"> Download </div> </div> <template for:each={recordList} for:item="rec" for:index="index"> <div key={rec.Id} class="slds-grid slds-wrap tableBody"> <div class="slds-col slds-size_1-of-12 slds-p-left_large slds-p-top_small"> {rec.count} </div> <div class="slds-col slds-size_4-of-12 slds-p-around_small slds-truncate displayInline"> <lightning-input type="checkbox" data-id="checkbox" value={rec.check} name={rec.ContentDocumentId} onchange={handleCheckbox} variant="label-hidden" style="min-width: 24px;"></lightning-input> <a target="_blank" onclick={navigateToRecordViewPage} data-key={rec.ContentDocumentId} class="slds-p-left_small slds-truncate">{rec.ContentDocument.Title}</a> </div> <div class="slds-col slds-size_2-of-12 slds-p-around_small slds-truncate"> {rec.size} </div> <div class="slds-col slds-size_2-of-12 slds-p-around_small"> {rec.ContentDocument.FileType} </div> <div class="slds-col slds-size_2-of-12 slds-p-around_small"> <lightning-formatted-date-time value={rec.ContentDocument.LastModifiedDate}></lightning-formatted-date-time> </div> <div class="slds-col slds-size_1-of-12 slds-p-around_x-small tableCell"> <lightning-button-icon name={rec.ContentDocumentId} icon-name="utility:download" variant="border-filled" alternative-text="download" onclick={downloadFile}></lightning-button-icon> </div> </div> </template> </template> <template if:false={recordList}> <p class="noDataDiv">No Files Found</p> </template> </div> </div> </template> <!-- Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE --> |
Now open the JS file massFilesDownload.js and paste the below code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
/* Code by CafeForce || www.cafeforce.com || support@cafeforce.com || Mandatory Header */ import { LightningElement, track, api } from 'lwc'; import { NavigationMixin } from 'lightning/navigation'; import { ShowToastEvent } from 'lightning/platformShowToastEvent'; import fetchObjectList from '@salesforce/apex/MassFilesDownloadController.fetchObjectList'; import fetchFiles from '@salesforce/apex/MassFilesDownloadController.fetchFiles'; export default class MassFilesDownload extends NavigationMixin(LightningElement) { @api objectApiName; @api recordId; @track objectOptions = []; @track yearOptions = []; @track monthOptions = []; @track selectedObject; @track selectedYear; @track selectedMonth; @track recordList; @track data; @track pageNumber = 1; @track recordSize = '10'; @track totalRecords; @track totalPages; @track showSpinner; @track selectedFiles = new Set(); connectedCallback() { if(this.recordId) { this.fetchDocumentRecords(); } else { this.showSpinner = true; fetchObjectList() .then(result => { this.objectOptions = result; var today = new Date(); var yearList = []; yearList.push({'label': (today.getFullYear() - 2).toString(), 'value': (today.getFullYear() - 2).toString()}); yearList.push({'label': (today.getFullYear() - 1).toString(), 'value': (today.getFullYear() - 1).toString()}); yearList.push({'label': today.getFullYear().toString(), 'value': today.getFullYear().toString()}); this.yearOptions = yearList; var monthList= []; for(var i = 1; i <= 12; i++) { monthList.push({'label': i.toString(), 'value': i.toString()}); } this.monthOptions = monthList; this.showSpinner = false; }).catch(error => { console.log(error); this.showSpinner = false; }) } } handleObjectChange(event) { this.selectedObject = event.detail.value; } handleYearChange(event) { this.selectedYear = event.detail.value; } handleMonthChange(event) { this.selectedMonth = event.detail.value; } get disableFetch() { if(!this.selectedObject) return true; } get totalFiles() { return this.data.length; } get recordSizeList() { let recordSizeList = []; recordSizeList.push({'label':'10', 'value':'10'}); recordSizeList.push({'label':'25', 'value':'25'}); recordSizeList.push({'label':'50', 'value':'50'}); recordSizeList.push({'label':'100', 'value':'100'}); return recordSizeList; } get disablePreviousButton() { if(!this.data || this.data.length == 0 || this.pageNumber == 1) return true; } get disableNextButton() { if(!this.data || this.data.length == 0 || this.pageNumber == this.totalPages) return true; } get disableRecordDropdown() { if(!this.data || this.data.length == 0) return true; } downloadFiles() { if(this.selectedFiles && this.selectedFiles.size > 0) { for(let item of this.selectedFiles) { this.download(item); } } else { this.showNotification('Please select a File', 'error'); } } handleCheckbox(event) { let checkbox = this.template.querySelectorAll('[data-id="headerCheckbox"]'); if(this.selectedFiles.has(event.target.name) && !event.target.checked) { checkbox[0].checked = false; this.selectedFiles.delete(event.target.name); } else { this.selectedFiles.add(event.target.name); if(this.selectedFiles.size == this.recordList.length) { checkbox[0].checked = true; } } } handleHeaderCheckbox(event) { let checkboxes = this.template.querySelectorAll('[data-id="checkbox"]') for(let i=0; i<checkboxes.length; i++) { checkboxes[i].checked = event.target.checked; } for(let i = 0; i < this.recordList.length; i++) { if(event.target.checked) { this.selectedFiles.add(this.recordList[i].ContentDocumentId); } else { this.selectedFiles.delete(this.recordList[i].ContentDocumentId); } } this.headerCheckbox = true; } resetCheckboxes() { let checkboxes = this.template.querySelectorAll('[data-id="checkbox"]') for(let i=0; i<checkboxes.length; i++) { checkboxes[i].checked = false; } let headerCheckbox = this.template.querySelectorAll('[data-id="headerCheckbox"]'); if(headerCheckbox && headerCheckbox.length > 0) headerCheckbox[0].checked = false; } handleRecordSizeChange(event) { this.recordSize = event.detail.value; this.pageNumber = 1; this.totalPages = Math.ceil(this.totalRecords / Number(this.recordSize)); this.processRecords(); } handleNavigation(event){ let buttonName = event.target.name; if(buttonName == 'Next') { this.pageNumber = this.pageNumber >= this.totalPages ? this.totalPages : this.pageNumber + 1; } else if(buttonName == 'Previous') { this.pageNumber = this.pageNumber > 1 ? this.pageNumber - 1 : 1; } this.processRecords(); } processRecords() { this.selectedFiles = new Set(); this.resetCheckboxes(); this.showSpinner = true; var uiRecords = []; var startLoop = ((this.pageNumber - 1) * Number(this.recordSize)); var endLoop = (this.pageNumber * Number(this.recordSize) >= this.totalRecords) ? this.totalRecords : this.pageNumber * Number(this.recordSize); for(var i = startLoop; i < endLoop; i++) { uiRecords.push(JSON.parse(JSON.stringify(this.data[i]))); } this.recordList = JSON.parse(JSON.stringify(uiRecords)); this.showSpinner = false; } fetchDocumentRecords() { this.showSpinner = true; this.pageNumber = 1; this.recordList = []; this.data = []; this.selectedFiles = new Set(); this.resetCheckboxes(); fetchFiles({ objectName : this.selectedObject, recordId : this.recordId, year : this.selectedYear, month : this.selectedMonth }) .then(result => { //console.log(result); if(result && result.length > 0) { for(var i = 0; i < result.length; i++) { var contentSize = result[i].ContentDocument.ContentSize; var size = (contentSize >= 1024) ? ((contentSize/1024 >= 1024) ? (Number(contentSize/1048576).toFixed(2) + ' MB') : (Number(contentSize/1024).toFixed(2) + ' KB')) : (Number(contentSize).toFixed(2) + ' Bytes'); result[i].size = size; result[i].count = i+1; result[i].check = false; } this.totalRecords = result.length; this.totalPages = Math.ceil(Number(result.length)/Number(this.recordSize)); this.data = JSON.parse(JSON.stringify(result)); var uiRecords = []; var recordDisplaySize = result.length < Number(this.recordSize) ? result.length : Number(this.recordSize); for(var i = 0; i < recordDisplaySize; i++) { uiRecords.push(JSON.parse(JSON.stringify(result[i]))); } this.recordList = JSON.parse(JSON.stringify(uiRecords)); } this.showSpinner = false; }).catch(error => { console.log(error); if(error && error.body && error.body.message) this.showNotification(error.body.message, 'error'); this.showSpinner = false; }) } showNotification(message, variant) { const evt = new ShowToastEvent({ 'message': message, 'variant': variant }); this.dispatchEvent(evt); } navigateToRecordViewPage(event) { if(event.currentTarget.dataset.key) { this[NavigationMixin.Navigate]({ type: 'standard__recordPage', attributes: { recordId: event.currentTarget.dataset.key, objectApiName: 'ContentDocument', actionName: 'view' } }); } } downloadFile(event) { if(event.target.name) { this.download(event.target.name); } } download(recordId) { this[NavigationMixin.Navigate]({ type: 'standard__webPage', attributes: { url: window.location.origin + '/sfc/servlet.shepherd/document/download/' + recordId } }, false); } } /* Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE */ |
Now paste the CSS in the massFilesDownload.css file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
.MFD { background: #fff; position: relative; } .MFD .headerTopDiv { background-color: #F3F2F2; } .MFD .headerText { font-size: 17px; } .MFD .tableHeader { font-weight: 600; color: #777; background-color: #FAFAF9; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; } .MFD .tableCell { padding: 5px; } .MFD .tableBody { border-bottom: 1px solid #ddd; } .MFD .tableBody:hover { background-color:#eee; } .MFD .bodyDiv { height: 265px; max-height: 300px; overflow: auto; } .MFD .displayInline { display: inline-flex; } .MFD .yearCombobox, .monthCombobox, .pageCombobox { width: 100px; } .MFD .noDataDiv { text-align: center; font-size: 16px; font-weight: 700; padding: 20px; } /* For Scrolling */ ::-webkit-scrollbar { width: 7px; height: 7px; } ::-webkit-scrollbar-track { display: none !important; } ::-webkit-scrollbar-thumb { border-radius: 10px; background: rgba(0,0,0,0.4); } |
Now open the meta file massFilesDownload.js-meta.xml and paste the below code.
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>49.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightning__AppPage</target> <target>lightning__RecordPage</target> <target>lightning__HomePage</target> </targets> </LightningComponentBundle> |
Step-3: Now we will add the component on record Lightning Page.
- Go to the record where you want to put this component.
- Click on Setting Icon on top and click on Edit Page.
- Now on the Lightning Builder drop the massFilesDownload component from the left pane onto the screen.
- Save the page and Activate for Org Default and click on the back button.
- Your component is visible now on the screen.
Finally, our component is ready to use. You can change the record display size in the component. You can either download a single Attachment or bulk select the attachment and click on the download button on top.
Also Check:
Nice Post, Really Helpful in downloading Bulk Attachments of any Object but showing error on few Objects that this Object is not supported.
Thank You Pooja for the Appreciation… 🙂 Yes, few Standard Objects are not supported to store Attachments. Although many useless objects are filtered still a few are left on which Content Document query or Attachment is not supported by Salesforce.
Hello,
I hope you are doing.
Could you please let me know if this supports Opportunity?
Thanks.
Hi Ayush,
Yes, it absolutely supports Opportunity.
Hi, how are you?
Could you please let me know if this supports Cases?
Thank you very much!
Hi Juan,
Definitely, Cases are supported by this component
Hey thank you so much, it was realy useful.
Do you know what adjustment do i need to implement for download all the files in a zip?
Thank you again, have a nice day.
Hi Juan,
Thanks for the appreciation.
For downloading all files into a zip, you need to use a third-party zip library in your code. I will share the code soon for the same.
Hi Suyash,
Do you have the code for downloading all the files into a zip?
Hi Regi,
No, Not yet. You have to use a third-party library for downloading files into the zip.
hi,
nice work! I thought about improving UX little bit and wonder if you know any possibility to prevent redirects to new tabs in browser when we download multiple files?
Hi Alex,
Thanks! You can use a third party library to download selected files into a Zip format.
Hi , Can you share the code on how to download all files as Zip
Hi Ram,
That code is not ready yet. You need to use a third-party library for that.
Hi Ram,
If you want to download the selected files as a zip, you need to adjust the URL like this:
/sfc/servlet.shepherd/document/download/ContentDocumentId/ContentDocumentId/ContentDocumentId
where each ContentDocumentId is the ID from the selected file you want to download. This works perfectly for me, I found out through this link below.
https://developer.salesforce.com/forums/?id=9060G0000005mu8QAA
Kind Regards,
Ryan
Do you have the test class for the controller?
Hi,
I don’t have a test class as of now. I will publish soon.
I have the exact requirement but for custom object, can you help if this can work on custom objects with few changes.