├── .forceignore ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── config └── project-scratch-def.json ├── force-app └── main │ └── default │ ├── classes │ ├── QM_Example_LWC_CreateTestDataController.cls │ ├── QM_Example_LWC_CreateTestDataController.cls-meta.xml │ ├── QM_Example_LWC_DataTableController.cls │ ├── QM_Example_LWC_DataTableController.cls-meta.xml │ ├── QM_Example_LWC_DeleteTestDataController.cls │ └── QM_Example_LWC_DeleteTestDataController.cls-meta.xml │ ├── flexipages │ └── QueryMore_Data_Table_Example.flexipage-meta.xml │ ├── lwc │ ├── .eslintrc.json │ ├── jsconfig.json │ ├── pubsub │ │ ├── pubsub.js │ │ └── pubsub.js-meta.xml │ ├── qmExampleCreateTestData │ │ ├── qmExampleCreateTestData.html │ │ ├── qmExampleCreateTestData.js │ │ └── qmExampleCreateTestData.js-meta.xml │ ├── qmExampleDataTable │ │ ├── qmExampleDataTable.html │ │ ├── qmExampleDataTable.js │ │ └── qmExampleDataTable.js-meta.xml │ └── qmExampleDeleteTestData │ │ ├── qmExampleDeleteTestData.html │ │ ├── qmExampleDeleteTestData.js │ │ └── qmExampleDeleteTestData.js-meta.xml │ ├── permissionsets │ └── QueryMore_Data_Table_Example.permissionset-meta.xml │ └── tabs │ └── QueryMore_Data_Table_Example.tab-meta.xml └── sfdx-project.json /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | **profiles 6 | package.xml 7 | 8 | # LWC configuration files 9 | **/jsconfig.json 10 | **/.eslintrc.json 11 | 12 | # LWC Jest 13 | **/__tests__/** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sfdx/ 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Dependency directories 16 | node_modules/ 17 | 18 | # Eslint cache 19 | .eslintcache 20 | 21 | # MacOS system files 22 | .DS_Store 23 | 24 | # Windows system files 25 | Thumbs.db 26 | ehthumbs.db 27 | [Dd]esktop.ini 28 | $RECYCLE.BIN/ 29 | 30 | # IDE files 31 | IlluminatedCloud 32 | Apex_QueryMore_Table_Example.iml 33 | .vscode 34 | .idea -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | .sfdx -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "overrides": [ 4 | { 5 | "files": "**/lwc/**/*.html", 6 | "options": { "parser": "lwc" } 7 | }, 8 | { 9 | "files": "*.{cmp,page,component}", 10 | "options": { "parser": "html" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apex QueryMore Example 2 | An example that demonstrates a technique for building "QueryMore" like functionality in Apex. 3 | 4 | The example showcases a table with infinite scrolling capabilities that retrieves records from the Salesforce database without relying on the SOQL OFFSET keyword. The technique allows for querying of records beyong the OFFSET keyword limit of 2000. 5 | 6 | This approach can also be applied to other use cases where large SOQL queries need to be fetched in chunks. 7 | 8 | **See this blog post for more information:** https://sfdc.danielzeidler.com/2019/08/18/building-querymore-functionality-in-apex-a-soql-offset-alternative/ 9 | 10 | ## Installation via SFDX 11 | 12 | 1. Create a scratch org: 13 | ``` 14 | sfdx force:org:create -s -f config/project-scratch-def.json -a query-more-example 15 | ``` 16 | 17 | 2. Push the app to your scratch org: 18 | ``` 19 | sfdx force:source:push 20 | ``` 21 | 22 | 2. Assign the **QueryMore Data Table Example** permission set to the default user: 23 | ``` 24 | sfdx force:user:permset:assign -n QueryMore_Data_Table_Example 25 | ``` 26 | 27 | 4. Open the scratch org: 28 | ``` 29 | sfdx force:org:open 30 | ``` 31 | 32 | 5. In App Launcher, select the **QueryMore Data Table Example** app. 33 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "Daniel Zeidler Company", 3 | "edition": "Developer", 4 | "features": [], 5 | "settings": { 6 | "orgPreferenceSettings": { 7 | "s1DesktopEnabled": true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /force-app/main/default/classes/QM_Example_LWC_CreateTestDataController.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * A controller for the qmExampleCreateTestData LWC 3 | */ 4 | public with sharing class QM_Example_LWC_CreateTestDataController { 5 | /** 6 | * Creates account records for use as test data 7 | */ 8 | @AuraEnabled 9 | public static void createTestData(Integer numberOfRecordsToCreate) { 10 | List accountsToInsert = new List(); 11 | 12 | for(Integer i = 1; i <= numberOfRecordsToCreate; i++) { 13 | accountsToInsert.add(new Account( 14 | Name = 'Test Account #' + i, 15 | NumberOfEmployees = Math.mod(i, 10) + 1 16 | )); 17 | } 18 | 19 | insert accountsToInsert; 20 | } 21 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/QM_Example_LWC_CreateTestDataController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/QM_Example_LWC_DataTableController.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * A controller for the qmExampleDataTable LWC 3 | */ 4 | public with sharing class QM_Example_LWC_DataTableController { 5 | /** 6 | * Gets a list of up to 50 Account records from the db 7 | * 8 | * @param sortedBy The field to sort the data by. Data will be sorted by Id if null. 9 | * @param sortedDirection Can be "asc" or "desc". Data is sorted by Id in ascending order if null. 10 | */ 11 | @AuraEnabled 12 | public static List getTestData(String sortedBy, String sortedDirection) { 13 | if(String.isEmpty(sortedBy) || String.isEmpty(sortedDirection)) { 14 | return queryTestDataSortedById(); 15 | } else { 16 | return queryTestDataSortedByAdditionalField(sortedBy, sortedDirection); 17 | } 18 | } 19 | 20 | /** 21 | * Gets up to an additional 25 Accounts from the db. 22 | * The lastId and lastValueOfSortedField parameters are used to determine the starting row offset of the returned dataset. 23 | * 24 | * @param sortedBy The API name of the field to sort by. Data will be sorted by Id if null 25 | * @param sortedDirection Can be "asc" or "desc". Data is sorted by Id in ascending order if null. 26 | * @param lastId The Id of the last row in the current dataset. 27 | * @param lastValueOfSortedField The value of the sortBy field in the last row of the current dataset. 28 | * Set to null if sorting by Id. 29 | * @param sortedFieldIsInteger Set to True if the sortBy field holds an integer, otherwise set to False. 30 | * This parameter is needed to work around an issue where the LWC sends us an 31 | * integer but Apex thinks it's a decimal 32 | */ 33 | @AuraEnabled 34 | public static List getMoreTestData(String sortedBy, String sortedDirection, Id lastId, Object lastValueOfSortedField, Boolean sortedFieldIsInteger) { 35 | if(String.isEmpty(sortedBy) || String.isEmpty(sortedDirection)) { 36 | return queryMoreTestDataSortedById(lastId); 37 | } else { 38 | return queryMoreTestDataSortedByAdditionalField(sortedBy, sortedDirection, lastId, lastValueOfSortedField, sortedFieldIsInteger); 39 | } 40 | } 41 | 42 | /** 43 | * Gets the first 50 Account records in the DB sorted by Id 44 | */ 45 | private static List queryTestDataSortedById() { 46 | return [ 47 | SELECT Name, NumberOfEmployees 48 | FROM Account 49 | ORDER BY Id 50 | LIMIT 50 51 | ]; 52 | } 53 | 54 | /** 55 | * Gets up to an additional 25 Accounts from the db sorted by Id. 56 | * 57 | * @param lastId The Id of the last row in the current dataset. 58 | * Used to determine the starting row offset of the returned dataset. 59 | */ 60 | private static List queryMoreTestDataSortedById(Id lastId) { 61 | return [ 62 | SELECT Name, NumberOfEmployees 63 | FROM Account 64 | WHERE Id > :lastId 65 | ORDER BY Id 66 | LIMIT 25 67 | ]; 68 | } 69 | 70 | /** 71 | * Gets up to 50 Account records sorted by a specified field and direction, from the db 72 | * 73 | * @param sortedBy The API name of the field to sort by 74 | * @param sortedDirection Can be "asc" or "desc". 75 | */ 76 | private static List queryTestDataSortedByAdditionalField(String sortedBy, String sortedDirection) { 77 | String queryString = 78 | 'SELECT Name, NumberOfEmployees ' 79 | + 'FROM Account ' 80 | + 'ORDER BY ' + sortedBy + ' ' + sortedDirection + ', Id ' 81 | + 'LIMIT 50'; 82 | 83 | return Database.query(queryString); 84 | } 85 | 86 | /** 87 | * Gets an additional 25 sorted Account records from the db. 88 | * The lastId and lastValueOfSortedField parameters are used to determine the starting row offset of the returned dataset. 89 | * 90 | * @param sortedBy The API name of the field to sort by. 91 | * @param sortedDirection Can be "asc" or "desc". 92 | * @param lastId The Id of the last row in the current dataset. 93 | * @param lastValueOfSortedField The value of the sortBy field in the last row of the current dataset. 94 | * @param sortedFieldIsInteger Set to True if the sortBy field holds an integer, otherwise set to False. 95 | * This parameter is needed to work around an issue where the LWC sends us an 96 | * integer but Apex thinks it's a decimal 97 | */ 98 | private static List queryMoreTestDataSortedByAdditionalField(String sortedBy, String sortedDirection, Id lastId, Object lastValueOfSortedField, Boolean sortedFieldIsInteger) { 99 | String directionOperator = sortedDirection == 'asc' ? '>' : '<'; 100 | 101 | // This hack is needed to avoid an issue where Integers sometimes come through as Decimals 102 | lastValueOfSortedField = sortedFieldIsInteger ? Integer.valueOf(lastValueOfSortedField) : lastValueOfSortedField; 103 | 104 | String queryString = 105 | 'SELECT Name, NumberOfEmployees ' 106 | + 'FROM Account ' 107 | + 'WHERE ' + sortedBy + ' ' + directionOperator + ' :lastValueOfSortedField ' 108 | + 'OR (' + sortedBy + ' = :lastValueOfSortedField AND Id > :lastId) ' 109 | + 'ORDER BY ' + sortedBy + ' ' + sortedDirection + ', Id LIMIT 25'; 110 | 111 | return Database.query(queryString); 112 | } 113 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/QM_Example_LWC_DataTableController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/QM_Example_LWC_DeleteTestDataController.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * A controller for the qmExampleDeleteTestData LWC 3 | */ 4 | public with sharing class QM_Example_LWC_DeleteTestDataController { 5 | @AuraEnabled 6 | public static void deleteTestData() { 7 | delete [SELECT Id FROM Account]; 8 | } 9 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/QM_Example_LWC_DeleteTestDataController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/flexipages/QueryMore_Data_Table_Example.flexipage-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | region1 5 | Region 6 | 7 | 8 | 9 | qmExampleCreateTestData 10 | 11 | 12 | qmExampleDeleteTestData 13 | 14 | region2 15 | Region 16 | 17 | 18 | 19 | qmExampleDataTable 20 | 21 | region3 22 | Region 23 | 24 | QueryMore Data Table Example 25 | 28 | AppPage 29 | 30 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": ".", 5 | "paths": { 6 | "c/pubsub": [ 7 | "pubsub/pubsub.js" 8 | ], 9 | "c/qmExampleCreateTestData": [ 10 | "qmExampleCreateTestData/qmExampleCreateTestData.js" 11 | ], 12 | "c/qmExampleDataTable": [ 13 | "qmExampleDataTable/qmExampleDataTable.js" 14 | ], 15 | "c/qmExampleDeleteTestData": [ 16 | "qmExampleDeleteTestData/qmExampleDeleteTestData.js" 17 | ] 18 | } 19 | }, 20 | "include": [ 21 | "**/*", 22 | "../../../../.sfdx/typings/lwc/**/*.d.ts" 23 | ], 24 | "typeAcquisition": { 25 | "include": [ 26 | "jest" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/pubsub/pubsub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A basic pub-sub mechanism for sibling component communication 3 | * 4 | * TODO - adopt standard flexipage sibling communication mechanism when it's available. 5 | */ 6 | 7 | const events = {}; 8 | 9 | const samePageRef = (pageRef1, pageRef2) => { 10 | const obj1 = pageRef1.attributes; 11 | const obj2 = pageRef2.attributes; 12 | return Object.keys(obj1) 13 | .concat(Object.keys(obj2)) 14 | .every(key => { 15 | return obj1[key] === obj2[key]; 16 | }); 17 | }; 18 | 19 | /** 20 | * Registers a callback for an event 21 | * @param {string} eventName - Name of the event to listen for. 22 | * @param {function} callback - Function to invoke when said event is fired. 23 | * @param {object} thisArg - The value to be passed as the this parameter to the callback function is bound. 24 | */ 25 | const registerListener = (eventName, callback, thisArg) => { 26 | // Checking that the listener has a pageRef property. We rely on that property for filtering purpose in fireEvent() 27 | if (!thisArg.pageRef) { 28 | throw new Error( 29 | 'pubsub listeners need a "@wire(CurrentPageReference) pageRef" property' 30 | ); 31 | } 32 | 33 | if (!events[eventName]) { 34 | events[eventName] = []; 35 | } 36 | const duplicate = events[eventName].find(listener => { 37 | return listener.callback === callback && listener.thisArg === thisArg; 38 | }); 39 | if (!duplicate) { 40 | events[eventName].push({ callback, thisArg }); 41 | } 42 | }; 43 | 44 | /** 45 | * Unregisters a callback for an event 46 | * @param {string} eventName - Name of the event to unregister from. 47 | * @param {function} callback - Function to unregister. 48 | * @param {object} thisArg - The value to be passed as the this parameter to the callback function is bound. 49 | */ 50 | const unregisterListener = (eventName, callback, thisArg) => { 51 | if (events[eventName]) { 52 | events[eventName] = events[eventName].filter( 53 | listener => 54 | listener.callback !== callback || listener.thisArg !== thisArg 55 | ); 56 | } 57 | }; 58 | 59 | /** 60 | * Unregisters all event listeners bound to an object. 61 | * @param {object} thisArg - All the callbacks bound to this object will be removed. 62 | */ 63 | const unregisterAllListeners = thisArg => { 64 | Object.keys(events).forEach(eventName => { 65 | events[eventName] = events[eventName].filter( 66 | listener => listener.thisArg !== thisArg 67 | ); 68 | }); 69 | }; 70 | 71 | /** 72 | * Fires an event to listeners. 73 | * @param {object} pageRef - Reference of the page that represents the event scope. 74 | * @param {string} eventName - Name of the event to fire. 75 | * @param {*} payload - Payload of the event to fire. 76 | */ 77 | const fireEvent = (pageRef, eventName, payload) => { 78 | if (events[eventName]) { 79 | const listeners = events[eventName]; 80 | listeners.forEach(listener => { 81 | if (samePageRef(pageRef, listener.thisArg.pageRef)) { 82 | try { 83 | listener.callback.call(listener.thisArg, payload); 84 | } catch (error) { 85 | // fail silently 86 | } 87 | } 88 | }); 89 | } 90 | }; 91 | 92 | export { 93 | registerListener, 94 | unregisterListener, 95 | unregisterAllListeners, 96 | fireEvent 97 | }; 98 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/pubsub/pubsub.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | false 5 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleCreateTestData/qmExampleCreateTestData.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleCreateTestData/qmExampleCreateTestData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface to create test data (Accounts) 3 | */ 4 | 5 | import {LightningElement, track, wire} from 'lwc'; 6 | import { fireEvent } from 'c/pubsub'; 7 | import { CurrentPageReference } from 'lightning/navigation'; 8 | import { ShowToastEvent } from 'lightning/platformShowToastEvent' 9 | import createTestData from '@salesforce/apex/QM_Example_LWC_CreateTestDataController.createTestData'; 10 | 11 | export default class QmExampleCreateTestData extends LightningElement { 12 | @wire(CurrentPageReference) pageRef; // for pubsub 13 | 14 | @track loading = false; // used for spinner 15 | numberOfTestRecordsToCreate = 125; 16 | 17 | HandleCreateTestDataButtonPress() { 18 | this.loading = true; 19 | createTestData({numberOfRecordsToCreate : this.numberOfTestRecordsToCreate}).then(() => { 20 | this.loading = false; 21 | this._showSuccessToast(); 22 | this._fireDataChangeEvent(); 23 | }); 24 | } 25 | 26 | handleNumberOfTestRecordsToCreate(event) { 27 | this.numberOfTestRecordsToCreate = event.target.value; 28 | } 29 | 30 | _fireDataChangeEvent() { 31 | fireEvent(this.pageRef, 'qm__testDataChange', {}); 32 | } 33 | 34 | _showSuccessToast() { 35 | const event = new ShowToastEvent({ 36 | title: 'Success!', 37 | message: `${this.numberOfTestRecordsToCreate} records inserted.`, 38 | variant: 'success' 39 | }); 40 | this.dispatchEvent(event); 41 | } 42 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleCreateTestData/qmExampleCreateTestData.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Qm Example Create Test Data 5 | true 6 | Qm Example Create Test Data 7 | 8 | lightning__AppPage 9 | 10 | 11 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleDataTable/qmExampleDataTable.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleDataTable/qmExampleDataTable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Displays Account records with the ability to infinitely load additional records and sort by any column 3 | * Account data is synonymous with "test data" in this module 4 | */ 5 | 6 | import {LightningElement, track, wire} from 'lwc'; 7 | import { CurrentPageReference } from 'lightning/navigation'; 8 | import { registerListener, unregisterAllListeners } from 'c/pubsub'; 9 | import getTestData from '@salesforce/apex/QM_Example_LWC_DataTableController.getTestData'; 10 | import getMoreTestData from '@salesforce/apex/QM_Example_LWC_DataTableController.getMoreTestData'; 11 | 12 | import NAME_FIELD from '@salesforce/schema/Account.Name'; 13 | import NUMBER_OF_EMPLOYEES_FIELD from '@salesforce/schema/Account.NumberOfEmployees'; 14 | 15 | export default class QmExampleDataTable extends LightningElement { 16 | @wire(CurrentPageReference) pageRef; // for pubsub 17 | 18 | /** lightning-datatable properties **/ 19 | @track data; 20 | @track columns = [ 21 | { 22 | label: 'Account Name', 23 | fieldName: NAME_FIELD.fieldApiName, 24 | type: 'text' 25 | }, 26 | { 27 | label: 'Number of Employees', 28 | fieldName: NUMBER_OF_EMPLOYEES_FIELD.fieldApiName, 29 | type: 'number', 30 | cellAttributes: { alignment: 'left' } 31 | } 32 | ]; 33 | @track sortedBy; 34 | @track sortedDirection; 35 | 36 | /** other properties **/ 37 | @track loadingInProgress = false; // used for turning the spinner on and off 38 | @track moreDataAvailableToLoad = true; 39 | 40 | get isLoadMoreButtonEnabled() { 41 | return !this.moreDataAvailableToLoad; 42 | } 43 | 44 | connectedCallback() { 45 | this._registerTestDataChangeListener(); 46 | this._refreshData(); 47 | } 48 | 49 | disconnectedCallback() { 50 | unregisterAllListeners(this); // for pubsub 51 | } 52 | 53 | handleLoadMoreDataButtonClick() { 54 | this._loadMoreDataIntoTable(); 55 | } 56 | 57 | handleEnableColumnSortCheckboxChange(event) { 58 | if(event.target.checked) { 59 | this._enableColumnSorting(); 60 | } else { 61 | this._disableColumnSorting(); 62 | } 63 | } 64 | 65 | handleSort(event) { 66 | this.sortedBy = event.detail.fieldName; 67 | this.sortedDirection = event.detail.sortDirection; 68 | this._refreshData(); 69 | } 70 | 71 | _registerTestDataChangeListener() { 72 | registerListener( 73 | 'qm__testDataChange', 74 | this._refreshData, 75 | this 76 | ); 77 | } 78 | 79 | /** 80 | * Refreshes the table with new data from the server 81 | * @private 82 | */ 83 | _refreshData() { 84 | this.loadingInProgress = true; 85 | this._fetchTestData().then((result) => { 86 | this.data = result; 87 | this.loadingInProgress = false; 88 | this.moreDataAvailableToLoad = true; 89 | }); 90 | } 91 | 92 | /** 93 | * fetches a list test of data 94 | * 95 | * @return {Promise} - A Promise for the fetched data 96 | * 97 | * @private 98 | */ 99 | _fetchTestData() { 100 | return getTestData({ 101 | sortedBy: this.sortedBy, 102 | sortedDirection : this.sortedDirection 103 | }); 104 | } 105 | 106 | /** 107 | * Appends additional test data to the table 108 | * @private 109 | */ 110 | _loadMoreDataIntoTable() { 111 | this.loadingInProgress = true; 112 | 113 | this._fetchMoreTestData().then((newData) => { 114 | if(newData.length === 0) { 115 | // TODO: change criteria when this is set to false. Current criteria leads to bad UX 116 | this.moreDataAvailableToLoad = false; 117 | } else { 118 | this.data = [...this.data, ...newData]; 119 | } 120 | this.loadingInProgress = false; 121 | }) 122 | } 123 | 124 | /** 125 | * Fetches additional data from the server 126 | * 127 | * @return {Promise} - A Promise for the additional data 128 | * @private 129 | */ 130 | _fetchMoreTestData() { 131 | const lastRow = this.data[this.data.length - 1]; 132 | 133 | return getMoreTestData({ 134 | sortedBy: this.sortedBy, 135 | sortedDirection : this.sortedDirection, 136 | lastId: lastRow.Id, 137 | lastValueOfSortedField: lastRow[this.sortedBy], 138 | 139 | // This hack is needed to avoid an issue with the LWC sending lastValueOfSortedField as a decimal value 140 | // when sorting by the NumberOfEmployees fields 141 | sortedFieldIsInteger: this.sortedBy === 'NumberOfEmployees' 142 | }); 143 | } 144 | 145 | _enableColumnSorting() { 146 | this._setColumnSortableProperty(true); 147 | 148 | this.sortedBy = 'Name'; 149 | this.sortedDirection = 'asc'; 150 | 151 | this._refreshData(); 152 | } 153 | 154 | _disableColumnSorting() { 155 | this._setColumnSortableProperty(false); 156 | 157 | this.sortedBy = null; 158 | this.sortedDirection = null; 159 | 160 | this._refreshData(); 161 | } 162 | 163 | /** 164 | * Sets all columns in the table and sortable or unsortable 165 | * 166 | * @param {boolean} isSortable - true if the columns should be sortable, false if not 167 | * @private 168 | */ 169 | _setColumnSortableProperty(isSortable) { 170 | this.columns = this.columns.map((column) => { 171 | column.sortable = isSortable; 172 | return column 173 | }); 174 | } 175 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleDataTable/qmExampleDataTable.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Qm Example Data Table 5 | true 6 | Qm Example Data Table 7 | 8 | lightning__AppPage 9 | 10 | 11 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleDeleteTestData/qmExampleDeleteTestData.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleDeleteTestData/qmExampleDeleteTestData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface to remove test data (Accounts) 3 | */ 4 | 5 | import {LightningElement, track, wire} from 'lwc'; 6 | import { CurrentPageReference } from 'lightning/navigation'; 7 | import deleteTestData from '@salesforce/apex/QM_Example_LWC_DeleteTestDataController.deleteTestData'; 8 | import {fireEvent} from "c/pubsub"; 9 | 10 | export default class QmExampleDeleteTestData extends LightningElement { 11 | @wire(CurrentPageReference) pageRef; // for pubsub 12 | 13 | @track loading = false; 14 | 15 | handleDeleteAllTestDataButtonPress() { 16 | this.loading = true; 17 | deleteTestData().then(() => { 18 | this.loading = false; 19 | this._fireDataChangeEvent(); 20 | }); 21 | } 22 | 23 | _fireDataChangeEvent() { 24 | fireEvent(this.pageRef, 'qm__testDataChange', {}); 25 | } 26 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/qmExampleDeleteTestData/qmExampleDeleteTestData.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Qm Example Delete Test Data 5 | true 6 | Qm Example Delete Test Data 7 | 8 | lightning__AppPage 9 | 10 | 11 | -------------------------------------------------------------------------------- /force-app/main/default/permissionsets/QueryMore_Data_Table_Example.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | 6 | QueryMore_Data_Table_Example 7 | Visible 8 | 9 | 10 | -------------------------------------------------------------------------------- /force-app/main/default/tabs/QueryMore_Data_Table_Example.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created by Lightning App Builder 4 | QueryMore_Data_Table_Example 5 | 6 | Custom1: Heart 7 | 8 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "namespace": "", 9 | "sfdcLoginUrl": "https://login.salesforce.com", 10 | "sourceApiVersion": "46.0" 11 | } 12 | --------------------------------------------------------------------------------