├── .eslintignore ├── .forceignore ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── RemoteSiteSetting.PNG ├── config └── project-scratch-def.json ├── force-app └── main │ └── default │ ├── classes │ ├── SObjectController2.cls │ ├── SObjectController2.cls-meta.xml │ ├── SObjectController2Test.cls │ └── SObjectController2Test.cls-meta.xml │ ├── flows │ ├── Datatable_Configuration_Helper.flow-meta.xml │ └── Datatable_Configuration_Helper_Temp_SubFlow.flow-meta.xml │ └── lwc │ ├── .eslintrc.json │ ├── datatableV2 │ ├── datatableV2.html │ ├── datatableV2.js │ └── datatableV2.js-meta.xml │ ├── jsconfig.json │ ├── jsconfig.json~master │ └── richDatatable │ ├── richDatatable.html │ ├── richDatatable.js │ ├── richDatatable.js-meta.xml │ └── richTextColumnType.html ├── manifest └── package.xml ├── package.json ├── scripts ├── apex │ └── hello.apex └── soql │ └── account.soql └── sfdx-project.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/lwc/**/*.css 2 | **/lwc/**/*.html 3 | **/lwc/**/*.json 4 | **/lwc/**/*.svg 5 | **/lwc/**/*.xml 6 | .sfdx -------------------------------------------------------------------------------- /.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 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__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 | .localdevserver/ 8 | 9 | # LWC VSCode autocomplete 10 | **/lwc/jsconfig.json 11 | 12 | # LWC Jest coverage reports 13 | coverage/ 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # Eslint cache 26 | .eslintcache 27 | 28 | # MacOS system files 29 | .DS_Store 30 | 31 | # Windows system files 32 | Thumbs.db 33 | ehthumbs.db 34 | [Dd]esktop.ini 35 | $RECYCLE.BIN/ 36 | -------------------------------------------------------------------------------- /.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 | **/staticresources/** 6 | .localdevserver 7 | .sfdx 8 | 9 | coverage/ -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "salesforce.salesforcedx-vscode", 4 | "redhat.vscode-xml", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Apex Replay Debugger", 9 | "type": "apex-replay", 10 | "request": "launch", 11 | "logFile": "${command:AskForLogFileName}", 12 | "stopOnEntry": true, 13 | "trace": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "**/bower_components": true, 5 | "**/.sfdx": true 6 | }, 7 | "salesforcedx-vscode-core.show-cli-success-msg": false 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DatatableV2 (Obsolete) 2 | 3 | Lightning Web Component for Flow Screens: **datatableV2** 4 | 5 | # The most recent source code can now be found here: 6 | https://github.com/alexed1/LightningFlowComponents/tree/master/flow_screen_components/datatable 7 | 8 | --- 9 | 10 | **This component allows the user to configure and display a datatable in a Flow screen.** 11 | 12 | Additional components packaged with this LWC: 13 | 14 | Apex Classes: SObjectController2 15 | SObjectController2Test 16 | 17 | Flows: Datatable Configuration Helper 18 | Datatable Configuration Helper - Temp SubFlow 19 | 20 | **Documentation:** https://unofficialsf.com/datatablev2-lightning-web-component-for-flow-screens/ 21 | 22 | **Created by:** Eric Smith 23 | **Date:** March 2020 24 | 25 | LinkedIn: https://www.linkedin.com/in/ericrsmith2 26 | Salesforce: https://trailblazer.me/id/ericsmith 27 | Blog: https://ericsplayground.wordpress.com/blog/ 28 | Twitter: https://twitter.com/esmith35 29 | 30 | --- 31 | **To just install datatableV2 without the Configuration helper, use these links: (v2.46)** 32 | Production/Developer: https://login.salesforce.com/packaging/installPackage.apexp?p0=04t3t000002kq5V 33 | Sandbox: https://test.salesforce.com/packaging/installPackage.apexp?p0=04t3t000002kq5V 34 | 35 | --- 36 | **You must install these components FIRST in order to use the Datatable Configuration Helper Flow** 37 | Flow Base Components (https://unofficialsf.com/introducing-flowbasecomponents/) 38 | 39 | 40 | Deploy to Salesforce 42 | 43 | 44 | --- 45 | Because the Datatable Configuration Helper uses Metadata APIs, you’ll need to have a Remote Site Setting on the org. If you don’t, you’ll see an error like this: 46 | 47 | `Metadata Transfer 48 | Job Status: Error: "IO Exception: Unauthorized endpoint, please check Setup->Security->Remote site settings. endpoint = https://test35-dev-ed--c.visualforce.com/services/Soap/m/42.0"` 49 | 50 | To address this, copy the root url from the error message and go to Setup –> Remote Site Settings and create a new setting. 51 | 52 | ![Remote Site Setting](RemoteSiteSetting.PNG?raw=true) 53 | 54 | This configures your org to essentially allow applications to run that call out to the internet and then back into the same org via its API endpoints. 55 | 56 | --- 57 | ## Release Notes 58 | 10/14/20 - Eric Smith - Version 2.47 - 59 | Bug Fix: Display correct icon for Table Header (was always showing standard:account icon) 60 | 61 | 10/07/20 - Eric Smith - Version 2.46 - 62 | Updates: Added new Output Parameter for the # of Selected Records 63 | (this can be used for conditional visibility on the same screen as the datatable) 64 | New Selected Record Output Parameter - Returns an SObject record if just a single record is selected 65 | New Required? Parameter - Requires the user to select at least 1 row to proceed 66 | New option to suppress the link for the object's standard Name field 67 | New optional Table Header with Table Icon and Table Label Parameters 68 | Switched DualListbox to the fbc version 69 | Added spinners while sorting & filtering data 70 | Allow case insensitive field API names 71 | Allow custom field API names w/o the __c suffix 72 | Bug Fixes: Display Picklist Labels instead of API Names for Picklist and Multipicklist fields 73 | Added a Clear Selection button for tables with just a single record 74 | 75 | 09/22/20 - Eric Smith - Version 2.45 - 76 | Bug Fix: Fixed inability to edit some field types (introduced by v2.44) 77 | 78 | 09/20/20 - Kevin Hart - Version 2.44 - 79 | Updates: Added ability to display Rich Text fields 80 | Eric Smith - Bug Fix: Fixed error when selecting column action of WrapText or ClipText 81 | 82 | 09/01/20 - Eric Smith - Version 2.43 - 83 | Bug Fix: Update Percent Field Handling and set Formula Fields to be Non-Editable 84 | 85 | 08/26/20 - Eric Smith - Version 2.42 - 86 | Bug Fix: Update Time fields with the User's Timezone Offset value so they display as intended 87 | Bug Fix: Fix field type so Datetime fields display correctly 88 | 89 | 08/14/20 - Eric Smith - Version 2.41 - 90 | Bug Fix: Fixed issue with time and date-time fields being displayed as a day earlier 91 | 92 | 08/11/20 - Eric Smith - Version 2.40 - 93 | Updates: Added attribute to allow the suppression of the record link on the SObject's 'Name' field 94 | Bug Fix: Fixed code so the 'Name' Field column now sorts correctly 95 | 96 | 07/31/20 - Eric Smith - Version 2.39 - 97 | Updates: Added Datatable Configuration Helper Flow 98 | REQUIRES: Flow Base Components (https://unofficialsf.com/introducing-flowbasecomponents/) 99 | REQUIRES: Dual List Box (https://unofficialsf.com/duallistbox/) 100 | REQUIRES: Remote Site Setting (Setup) 101 | 102 | 07/31/20 - Andy Hass - Version 2.38 - 103 | Updates: Added support for Checkbox Field Type 104 | 105 | 07/07/20 - Eric Smith - Version 2.37 - 106 | Bug Fix: Fixed issue with the date being displayed as a day earlier 107 | 108 | 07/01/20 - Eric Smith - Version 2.36 - 109 | Updates: Now displays the primary "Name" field as a Link (textWrap = true) 110 | Added button in Config Mode to round off Column Width values 111 | 112 | 06/30/20 - Eric Smith - Version 2.35 - 113 | Updates: Extended Configuration Mode to handle Column Alignments, Labels, Widths, Allow Edit & Allow Filter 114 | Added Configuration Mode buttons to select all columns for Edit and/or Filter 115 | Selecting an attribute string now copies the contents into the system Clipboard 116 | 117 | 06/24/20 - Eric Smith - Version 2.34 - 118 | Updates: Added Configuration Mode parameter (Used by Datatable Configuration Flow) 119 | Bug Fix: Fixed issue with column widths resetting when filtering 120 | 121 | 06/19/20 - Eric Smith - Version 2.33 - 122 | Updates: Removed default value for Table Height 123 | Bug Fix: Fixed issue with lookup fields being blank in the first record 124 | 125 | 06/03/20 - Eric Smith - Version 2.32 - 126 | Bug Fix: Fixed error when editing more than one column in a single row while suppressing the Cancel/Save buttons 127 | 128 | 06/03/20 - Eric Smith - Version 2.31 - 129 | Updates: Changed SObjectController to SObjectController2 to allow for easier deployment to orgs 130 | that already have an earlier version of the datatable component 131 | 132 | 06/03/20 - Eric Smith - Version 2.3 - 133 | Updates: Changed SObjectController to SObjectController2 to allow for easier deployment to orgs 134 | that already have an earlier version of the datatable component 135 | 136 | 06/03/20 - Eric Smith - Version 2.2 - 137 | Enhancements: Added datatable border attribute 138 | Updates: Fixed attribute parsing, Fixed Spinner 139 | 140 | 06/01/20 - Eric Smith - Version 2.1 - 141 | Enhancements: Updated with features from v1.2 & v1.3 142 | 143 | 04/22/20 - Eric Smith - Version 2.0 (Summer '20) - 144 | Enhancements: Summer '20 New Feature - Dynamic Object Type 145 | One version of the component now supports ALL Standard & Custom SObjects 146 | 147 | 05/23/20 - Eric Smith - Version 1.3 - 148 | Updates: Added support for a serialized JSON string of records of a user defined object 149 | Added an attribute to specify the height of the datatable 150 | Bug Fix - Fixed error when editing multiple rows 151 | 152 | 05/06/20 - Eric Smith - Version 1.2 - 153 | Updates: Handle lookup Objects without a Name field & 154 | Trap non-updatable Master/Detail fields 155 | 156 | 04/14/20 - Eric Smith - Version 1.1 - 157 | Enhancements: New Column Attribute to support column filtering 158 | 159 | 04/01/20 - Eric Smith - Version 1.0 - 160 | Features: The only required paramters are the SObject collection of records and a list of field API names 161 | The field label and field type will default to what is defined in the object 162 | Numeric fields will display with the correct number of decimal places as defined in the object 163 | Lookup fields are supported and will display the referenced record's name field as a clickable link 164 | All columns are sortable, including lookups (by name) 165 | The selection column can be multi-select (Checkboxes), single-select (Radio Buttons), or hidden 166 | A collection of pre-selected rows can be passed into the component 167 | Inline editing is supported with changed values passed back to the flow 168 | Unlike the original datatable component, only the edited records will be passed back to the flow 169 | The maximum number of rows to display can be set by the user 170 | Optional attribute overrides are supported and can be specified by list, column # or by field name, including: 171 | 172 | - Alignment 173 | - Editable 174 | - Header Icon 175 | - Header Label 176 | - Initial Column Width 177 | - Custom Cell Attributes including those with nested values {name: {name:value}} 178 | - Custom Type Attributes including those with nested values {name: {name:value}} 179 | - Other Custom Column Attributes including those with nested values {name: {name:value}} 180 | 181 | --- 182 | 183 | Copyright (c) 2020, Eric Smith 184 | 185 | Redistribution and use in source and binary forms, with or without modification, are permitted provided 186 | that the following conditions are met: 187 | 188 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 189 | 190 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 191 | in the documentation and/or other materials provided with the distribution. 192 | 193 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived 194 | from this software without specific prior written permission. 195 | 196 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 197 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 198 | SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 199 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 200 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 201 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | -------------------------------------------------------------------------------- /RemoteSiteSetting.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericrsmith35/DatatableV2/cfe058dbef785d2169feee7a1db31d4f9bc73177/RemoteSiteSetting.PNG -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "Demo company", 3 | "edition": "Developer", 4 | "features": [], 5 | "settings": { 6 | "lightningExperienceSettings": { 7 | "enableS1DesktopEnabled": true 8 | }, 9 | "securitySettings": { 10 | "passwordPolicies": { 11 | "enableSetPasswordInApi": true 12 | } 13 | }, 14 | "mobileSettings": { 15 | "enableS1EncryptedStoragePref2": false 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SObjectController2.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Based on a component (ItemsToApprove) created by: Alex Edelstein (Salesforce) 4 | * Based on a component (FlatTable) created by: J. Pipkin (OpFocus, Inc) 5 | * 6 | * Description: getColumnData 7 | * Get field information from a list of field names in order to build 8 | * the column definitions for the datatable 9 | * 10 | * getLookupData 11 | * For each lookup type field get the related object and "Name" field 12 | * 13 | * getRowData 14 | * Take a List of Records and a List of Lookup Field Names and 15 | * use the recordId values in the lookup fields get the values 16 | * of the Name fields in the corresponding records. Return the 17 | * records that now include both the Id and Name for each lookup. 18 | * 19 | * 10/xx/20 - Eric Smith - Version 2.46 Issue error if no field names are given 20 | * Allow case insensitive field names 21 | * Allow custom field API names w/o the __c suffix 22 | * Get and Return which fields are picklist fields along with Value & Label 23 | * 24 | * 09/22/20 - Eric Smith - Version 2.45 Set type as Richtext for Text Formula Fields 25 | * 26 | * 09/01/20 - Eric Smith - Version 2.43 Update Percent Field Handling and set Formula Fields to be Non-Editable 27 | * 28 | * 08/26/20 - Eric Smith - Version 2.42 Get and return User's Timezone Offset so Time fields can be adjusted 29 | * 30 | * 07/07/20 - Eric Smith - Version 2.37 Fixed date displaying as a day earlier 31 | * 32 | * 07/01/20 - Eric Smith - Version 2.36 Added a return value for the "Name" field of the SObject 33 | * This is used to display that field as a Link in the Datatable 34 | * 35 | * 06/19/20 - Eric Smith - Version 2.33 Fixed issue with lookup fields being blank in the first record 36 | * Renumbered to match datatableV2 versioning 37 | * 38 | * 06/03/20 - Eric Smith - Version 2.0 Renamed to allow for easier installation with datatableV2 39 | * 40 | * 04/28/20 - Eric Smith - Version 1.2 Handle lookup Objects without a Name field & 41 | * Trap non-updatable Master/Detail fields 42 | * 43 | * 04/14/20 - Eric Smith - Version 1.1 Cleaned up some error handling 44 | * 45 | * 04/01/20 - Eric Smith - Version 1.0 46 | * 47 | **/ 48 | 49 | public with sharing class SObjectController2 { 50 | 51 | // this is just a convenient way to return multiple unique pieces of data to the component 52 | public class ReturnResults { 53 | List rowData; 54 | String dtableColumnFieldDescriptorString; 55 | String lookupFieldData; 56 | List lookupFieldList; 57 | Map> dataMap; 58 | Map objNameFieldMap; 59 | Map> picklistFieldMap; // Field API Name, 60 | List percentFieldList; 61 | List noEditFieldList; 62 | list timeFieldList; 63 | list picklistFieldList; 64 | String objectName; 65 | String objectLinkField; 66 | String timezoneOffset; 67 | } 68 | 69 | @AuraEnabled 70 | public static string getReturnResults(List records, String fieldNames){ 71 | System.Debug('records-'+records); 72 | System.Debug('fieldNames-'+fieldNames); 73 | ReturnResults curRR = new ReturnResults(); 74 | if (records.isEmpty()) { 75 | // throw new MyApexException ('The datatable record collection is empty'); 76 | List emptyList = new List(); 77 | curRR.dtableColumnFieldDescriptorString = '{"label":"Empty Table, Fields ['+fieldNames+']", "fieldName":"Id", "type":"text"}'; 78 | curRR.lookupFieldData = '{}'; 79 | curRR.lookupFieldList = emptyList; 80 | curRR.percentFieldList = emptyList; 81 | curRR.noEditFieldList = emptyList; 82 | curRR.timeFieldList = emptyList; 83 | curRR.picklistFieldList = emptyList; 84 | curRR.rowData = records; 85 | curRR.objectName = 'EmptyCollection'; 86 | curRR.objectLinkField = ''; 87 | } else { 88 | String objName = records[0].getSObjectType().getDescribe().getName(); 89 | curRR = getColumnData(curRR, fieldNames, objName); 90 | curRR = getLookupData(curRR, records, curRR.lookupFieldList, objName); 91 | curRR = getRowData(curRR, records, curRR.dataMap, curRR.objNameFieldMap, curRR.lookupFieldList, curRR.percentFieldList, objName, curRR.noEditFieldList); 92 | curRR.objectName = objName; 93 | } 94 | curRR.timezoneOffset = getTimezoneOffset().format(); 95 | System.Debug('curRR - '+JSON.serializePretty(curRR)); 96 | return JSON.serialize(curRR); 97 | } 98 | 99 | @AuraEnabled 100 | public static ReturnResults getColumnData(ReturnResults curRR, String fields, String objName) { 101 | 102 | if (fields == '') 103 | throw new MyApexException('You must specify at least one field API name from the object ' + objName); 104 | 105 | SObjectType sobjType = ((SObject)(Type.forName('Schema.'+objName).newInstance())).getSObjectType(); 106 | DescribeSObjectResult objDescribe = sobjType.getDescribe(); 107 | 108 | String datatableColumnFieldDescriptor = ''; 109 | String fieldType = ''; 110 | List curFieldDescribes = new List(); 111 | String lookupFieldData = ''; 112 | List lookupFields = new List(); 113 | List percentFields = new List(); 114 | List noEditFields = new List(); 115 | List timeFields = new List(); 116 | List picklistFields = new List(); 117 | Map> picklistFieldLabels = new Map>(); 118 | String objectLinkField = getNameUniqueField(objName); // Name (link) Field for the Datatable SObject 119 | System.debug('*** OBJ/LINK' + objname + '/' + objectLinkField); 120 | 121 | for (String fieldName : fields.split(',')) { 122 | 123 | Map fieldMap = objDescribe.fields.getMap(); 124 | Schema.SObjectField fieldItem = fieldMap.get(fieldName); 125 | if (fieldItem == null) { 126 | Schema.SObjectField fieldItem2 = fieldMap.get(fieldName + '__c'); // Allow for user to forget to add __c for custom fields 127 | if (fieldItem2 == null) { 128 | throw new MyApexException('Could not find the field: ' + fieldName + ' on the object ' + objName); 129 | } else { 130 | fieldItem = fieldItem2; 131 | } 132 | } 133 | Schema.DescribeFieldResult dfr = fieldItem.getDescribe(); 134 | curFieldDescribes.add(dfr); 135 | 136 | datatableColumnFieldDescriptor = datatableColumnFieldDescriptor 137 | + ',{"label" : "' + dfr.getLabel() 138 | + '", "fieldName" : "' + dfr.getName() // pass back correct API name if user did not pass in correct case (Name vs name) 139 | + '", "type" : "' + convertType(dfr.getType().name(), dfr.isCalculated()) 140 | + '", "scale" : "' + dfr.getScale() 141 | + '"}'; 142 | 143 | if (!dfr.isUpdateable() || dfr.isCalculated()) noEditFields.add(fieldName); // Check for Read Only and Formula fields 144 | 145 | switch on dfr.getType().name() { 146 | when 'REFERENCE' { 147 | if (dfr.isUpdateable()) { // Only works with Master-Detail fields that are reparentable 148 | lookupFields.add(fieldName); 149 | } 150 | } 151 | when 'PERCENT' { 152 | percentFields.add(fieldName); 153 | } 154 | when 'TEXTAREA' { 155 | if (!dfr.isSortable() && !noEditFields.contains(fieldName)) { 156 | noEditFields.add(fieldName); // Long Text Area and Rich Text Area 157 | } 158 | } 159 | when 'ENCRYPTEDSTRING' { 160 | if (!noEditFields.contains(fieldName)) { 161 | noEditFields.add(fieldName); 162 | } 163 | } 164 | when 'CURRENCY', 'DECIMAL', 'DOUBLE', 'INTEGER', 'LONG' { 165 | // *** create scale attrib in datatableColumnFieldDescriptor and pass the getScale() values in that way. *** 166 | } 167 | when 'TIME' { 168 | timeFields.add(fieldName); 169 | } 170 | when 'PICKLIST', 'MULTIPICKLIST' { 171 | picklistFields.add(dfr.getName()); 172 | if (!noEditFields.contains(fieldName)) { 173 | noEditFields.add(fieldName); 174 | } 175 | Map valueLabelPair = new Map(); 176 | for(Schema.PicklistEntry ple : dfr.getPicklistValues()) { 177 | valueLabelPair.put(ple.getValue(), ple.getLabel()); 178 | } 179 | picklistFieldLabels.put(dfr.getName(), valueLabelPair); 180 | } 181 | when else { 182 | } 183 | } 184 | } 185 | 186 | System.debug('final fieldDescribe string is: ' + datatableColumnFieldDescriptor); 187 | curRR.dtableColumnFieldDescriptorString = datatableColumnFieldDescriptor.substring(1); // Remove leading , 188 | curRR.lookupFieldData = lookupFieldData; 189 | curRR.lookupFieldList = lookupFields; 190 | curRR.percentFieldList = percentFields; 191 | curRR.noEditFieldList = noEditFields; 192 | curRR.timeFieldList = timeFields; 193 | curRR.picklistFieldList = picklistFields; 194 | curRR.picklistFieldMap = picklistFieldLabels; 195 | curRR.objectLinkField = objectLinkField; 196 | return curRR; 197 | } 198 | 199 | @AuraEnabled 200 | public static ReturnResults getLookupData(ReturnResults curRR, List records, List lookupFields, String objName){ 201 | 202 | // Get names of the related objects 203 | Map> objIdMap = new Map>(); 204 | for(SObject so : records) { 205 | for(String lf : lookupFields) { 206 | if(so.get(lf) != null) { 207 | Id lrid = ((Id) so.get(lf)); 208 | String relObjName = lrid.getSobjectType().getDescribe().getName(); 209 | if(!objIdMap.containsKey(relObjName)) { 210 | objIdMap.put(relObjName, new Set()); 211 | } 212 | objIdMap.get(relObjName).add(lrid); 213 | } 214 | } 215 | } 216 | 217 | // Lookup the "Name" field in the related object 218 | Map> dataMap = new Map>(); 219 | Map objNameFieldMap = new Map(); 220 | for(String obj : objIdMap.keySet()) { 221 | Set ids = objIdMap.get(obj); 222 | String nameField = getNameUniqueField(obj); 223 | SObject[] recs = Database.query('Select Id, ' + nameField + ' from ' + obj + ' where Id in :ids'); 224 | System.Debug('Name Field: '+obj+' - '+nameField); 225 | Map somap = new Map(); 226 | for(SObject so : recs) { 227 | somap.put((Id) so.get('Id'), so); 228 | } 229 | dataMap.put(obj, somap); 230 | objNameFieldMap.put(obj, nameField); 231 | } 232 | 233 | curRR.dataMap = dataMap; 234 | curRR.objNameFieldMap = objNameFieldMap; 235 | return curRR; 236 | } 237 | 238 | @AuraEnabled 239 | public static ReturnResults getRowData(ReturnResults curRR, List records, Map> dataMap, Map objNameFieldMap, List lookupFields, List percentFields, String objName, List noEditFields) { 240 | // Update object to include values for the "Name" field referenced by Lookup fields 241 | String lookupFieldData = ''; 242 | Map firstRecord = new Map(); 243 | for(String lf : lookupFields) { 244 | firstRecord.put(lf,true); 245 | } 246 | 247 | for(SObject so : records) { 248 | 249 | // Divide percent field values by 100 250 | // for(String pf : percentFields) { 251 | // if(so.get(pf) != null && !noEditFields.contains(pf)) { 252 | // so.put(pf, double.valueOf(so.get(pf))/100); 253 | // } 254 | // } 255 | 256 | // Add new lookup field values 257 | for(String lf : lookupFields) { 258 | if(so.get(lf) != null) { 259 | Id lrid = ((Id) so.get(lf)); 260 | String relObjName = lrid.getSobjectType().getDescribe().getName(); 261 | Map recs = dataMap.get(relObjName); 262 | if (recs == null) continue; 263 | SObject cso = recs.get(lrid); 264 | if (cso == null) continue; 265 | String relName; 266 | if (lf.toLowerCase().endsWith('id')) { 267 | relName = lf.replaceAll('(?i)id$', ''); 268 | } else { 269 | relName = lf.replaceAll('(?i)__c$', '__r'); 270 | } 271 | so.putSObject(relName, cso); 272 | 273 | // Save the Object and "Name" field related to the lookup field 274 | if(firstRecord.get(lf)) { 275 | lookupFieldData = lookupFieldData 276 | + ',{ "object" : "' + relObjName 277 | + '", "fieldName" : "' + relName 278 | + '", "nameField" : "' + objNameFieldMap.get(relObjName) 279 | + '"}'; 280 | firstRecord.put(lf,false); 281 | } 282 | } 283 | } 284 | } 285 | 286 | // return lookup field info and records; 287 | curRR.lookupFieldData = (lookupFieldData.length() > 0) ? lookupFieldData.substring(1) : ''; // Remove leading , 288 | curRR.rowData = records; 289 | return curRR; 290 | } 291 | 292 | public class MyApexException extends Exception { 293 | } 294 | 295 | //convert the apex type to the corresponding javascript type that datatable will understand 296 | private static String convertType (String apexType, Boolean isFormula){ 297 | switch on apexType { 298 | when 'BOOLEAN' { 299 | return 'boolean'; 300 | } 301 | when 'CURRENCY' { 302 | return 'currency'; 303 | } 304 | when 'DATE' { 305 | return 'date-local'; 306 | } 307 | when 'DATETIME' { 308 | return 'datetime'; // Custom type for this component 309 | } 310 | when 'DECIMAL', 'DOUBLE', 'INTEGER', 'LONG' { 311 | return 'number'; 312 | } 313 | when 'EMAIL' { 314 | return 'email'; 315 | } 316 | when 'ID' { 317 | return 'id'; 318 | } 319 | when 'LOCATION' { 320 | return 'location'; 321 | } 322 | when 'PERCENT' { 323 | return 'percent'; 324 | } 325 | when 'PHONE' { 326 | return 'phone'; 327 | } 328 | when 'REFERENCE' { 329 | return 'lookup'; // Custom type for this component 330 | } 331 | when 'TIME' { 332 | return 'time'; // Custom type for this component 333 | } 334 | when 'URL' { 335 | return 'url'; 336 | } 337 | when 'CHECKBOX' { 338 | return 'checkbox'; 339 | } 340 | when 'STRING' { 341 | if (isFormula) return 'richtext'; 342 | return 'text'; 343 | } 344 | when else { 345 | // throw new MyApexException ('you\'ve specified the unsupported field type: ' + apexType ); 346 | return 'text'; 347 | } 348 | } 349 | } 350 | 351 | //Get the 'Name' field for the given SObjectType 352 | private static String getNameUniqueField(String objectName) { 353 | String strResult = null; 354 | SObjectType sobjType = ((SObject)(Type.forName('Schema.'+objectName).newInstance())).getSObjectType(); 355 | DescribeSObjectResult objDescribe = sobjType.getDescribe(); 356 | Map fieldMap = objDescribe.fields.getMap(); 357 | for(String fieldName : fieldMap.keySet()) { 358 | SObjectField objField = fieldMap.get(fieldName); 359 | Schema.DescribeFieldResult dfr = objField.getDescribe(); 360 | if(dfr.isNameField()) { 361 | strResult = dfr.getName(); 362 | break; 363 | } 364 | if(strResult != null) { 365 | return strResult; 366 | } 367 | } 368 | for(String fieldName : fieldMap.keySet()) { 369 | SObjectField objField = fieldMap.get(fieldName); 370 | Schema.DescribeFieldResult dfr = objField.getDescribe(); 371 | if(dfr.isAutoNumber()) { 372 | strResult = dfr.getName(); 373 | break; 374 | } 375 | if(strResult != null) { 376 | return strResult; 377 | } 378 | } 379 | for(String fieldName : fieldMap.keySet()) { 380 | SObjectField objField = fieldMap.get(fieldName); 381 | Schema.DescribeFieldResult dfr = objField.getDescribe(); 382 | if(dfr.isUnique()) { 383 | strResult = dfr.getName(); 384 | break; 385 | } 386 | } 387 | return strResult; 388 | } 389 | 390 | // Get the offset value between GMT and the running User's timezone 391 | private static integer getTimezoneOffset() { 392 | Datetime dtNow = Datetime.now(); 393 | return UserInfo.getTimezone().getOffset(dtNow); 394 | } 395 | 396 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/SObjectController2.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SObjectController2Test.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class SObjectController2Test { 3 | 4 | static testMethod void test() { 5 | Account a1 = new Account(Name='Test1', 6 | AccountNumber='1', 7 | Website='https://trailblazer.me/id/ericsmith', 8 | Type='Type1', 9 | Description='D1'); 10 | insert a1; 11 | Account a2 = new Account(Name='Test2', 12 | AccountNumber='2', 13 | Website='https://ericsplayground.wordpress.com/blog/', 14 | Type='Type2', 15 | Description='D2'); 16 | insert a2; 17 | 18 | Account[] accts = [Select Id, Name, OwnerId from Account]; 19 | String fieldnames = 'Name,Id,OwnerId,AccountNumber,Website,Type,Description,IsDeleted,CreatedDate,AnnualRevenue,Fax,LastActivityDate'; 20 | String testResponse = SObjectController2.getReturnResults(accts, fieldnames); 21 | System.assert(testResponse.contains('"noEditFieldList":[')); 22 | System.assert(testResponse.contains('"lookupFieldList":["OwnerId"]')); 23 | 24 | Account[] empty = [Select Id, Name, OwnerId from Account Where Name='NotInAccounts']; 25 | String testEmpty = SObjectController2.getReturnResults(empty, fieldnames); 26 | System.assert(testEmpty.contains('"objectName":"EmptyCollection"')); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/SObjectController2Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/flows/Datatable_Configuration_Helper.flow-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Get_Field_Information_fbc 5 | 6 | 551 7 | 423 8 | fbc_GetFieldInformation 9 | apex 10 | 11 | Select_Fields 12 | 13 | 14 | objectName 15 | 16 | PickObject.objectType 17 | 18 | 19 | 20 | apexColFieldDescriptors_fbc 21 | fields 22 | 23 | 24 | 25 | Set_Field_List_Parameter 26 | 27 | 694 28 | 203 29 | 30 | vFieldList 31 | Assign 32 | 33 | List_of_Field_API_Names 34 | 35 | 36 | 37 | Deploy_Flow 38 | 39 | 40 | 41 | Any_Fields_Selected 42 | 43 | 875 44 | 473 45 | 46 | Get_Field_Information_fbc 47 | 48 | NONE 49 | 50 | YES_fs 51 | and 52 | 53 | fListLength 54 | GreaterThan 55 | 56 | 0.0 57 | 58 | 59 | 60 | Deploy_Flow 61 | 62 | 63 | 64 | 65 | 66 | Did_User_Enter_Field_List 67 | 68 | 543 69 | 205 70 | Default Outcome 71 | 72 | ObjectMissing 73 | and 74 | 75 | PickObject.objectType 76 | IsNull 77 | 78 | true 79 | 80 | 81 | 82 | Object_Picker 83 | 84 | 85 | 86 | 87 | YES 88 | and 89 | 90 | List_of_Field_API_Names 91 | NotEqualTo 92 | 93 | 94 | 95 | 96 | 97 | Set_Field_List_Parameter 98 | 99 | 100 | 101 | 102 | NO 103 | and 104 | 105 | List_of_Field_API_Names 106 | EqualTo 107 | 108 | 109 | 110 | 111 | 112 | Get_Field_Information_fbc 113 | 114 | 115 | 116 | 117 | Run this Flow to select a list of fields to feed into your Datatable flow component. After selecting your fields you can view a sample table to work with to adjust column widths and change labels. 118 | 119 | fListLength 120 | Number 121 | LEN({!vFieldList}) 122 | 0 123 | 124 | Datatable Configuration Helper {!$Flow.CurrentDateTime} 125 | 126 | 127 | BuilderType 128 | 129 | LightningFlowBuilder 130 | 131 | 132 | 133 | OriginBuilderType 134 | 135 | LightningFlowBuilder 136 | 137 | 138 | Flow 139 | 140 | Deploy_Flow 141 | 142 | 814 143 | 203 144 | true 145 | true 146 | false 147 | 148 | Run_New_SubFlow 149 | 150 | 151 | PleaseWaitMsg 152 | <p><b style="font-size: 16px;">Please wait while the next section of this Flow is deployed.</b></p><p><br></p> 153 | DisplayText 154 | 155 | 156 | debug 157 | <p>Selected Object: {!PickObject.objectType}</p><p>Selected Fields: {!vFieldList}</p> 158 | DisplayText 159 | 160 | 161 | DeployMetadata 162 | c:fbc_transferMetadata 163 | ComponentInstance 164 | 165 | metadataName 166 | 167 | Datatable_Configuration_Helper_Temp_SubFlow 168 | 169 | 170 | 171 | metadataString 172 | 173 | DatatableConfigFlow 174 | 175 | 176 | 177 | objectType 178 | 179 | Flow 180 | 181 | 182 | 183 | transferMode 184 | 185 | deploy 186 | 187 | 188 | true 189 | true 190 | 191 | false 192 | true 193 | 194 | 195 | Object_Picker 196 | 197 | 396 198 | 203 199 | false 200 | true 201 | false 202 | 203 | Did_User_Enter_Field_List 204 | 205 | 206 | PickObject 207 | c:fbc_pickObjectAndField 208 | ComponentInstance 209 | 210 | hideFieldPicklist 211 | 212 | true 213 | 214 | 215 | 216 | masterLabel 217 | 218 | Select the Object for your Datatable 219 | 220 | 221 | 222 | objectLabel 223 | 224 | Select the Object for your Datatable Records 225 | 226 | 227 | true 228 | true 229 | 230 | 231 | FieldOption 232 | <p><br></p><p>You can enter your own comma separated list of Field API Names below or just click Next to select your fields individually.</p> 233 | DisplayText 234 | 235 | and 236 | 237 | PickObject.objectType 238 | IsNull 239 | 240 | false 241 | 242 | 243 | 244 | 245 | 246 | List_of_Field_API_Names 247 | String 248 | List of Field API Names 249 | InputField 250 | false 251 | 252 | and 253 | 254 | PickObject.objectType 255 | IsNull 256 | 257 | false 258 | 259 | 260 | 261 | 262 | true 263 | true 264 | 265 | 266 | Select_Fields 267 | 268 | 694 269 | 342 270 | true 271 | true 272 | false 273 | 274 | Any_Fields_Selected 275 | 276 | 277 | Object 278 | <p><b style="font-size: 18px;">{!PickObject.objectType}</b></p> 279 | DisplayText 280 | 281 | 282 | SelectFields 283 | c:fbc_dualListBox2 284 | ComponentInstance 285 | 286 | allOptionsStringFormat 287 | 288 | object 289 | 290 | 291 | 292 | useWhichObjectKeyForData 293 | 294 | name 295 | 296 | 297 | 298 | required 299 | 300 | true 301 | 302 | 303 | 304 | useWhichObjectKeyForSort 305 | 306 | label 307 | 308 | 309 | 310 | label 311 | 312 | Select and Order your Datatable Fields 313 | 314 | 315 | 316 | sourceLabel 317 | 318 | Available 319 | 320 | 321 | 322 | selectedLabel 323 | 324 | Selected 325 | 326 | 327 | 328 | min 329 | 330 | 1.0 331 | 332 | 333 | 334 | max 335 | 336 | 20.0 337 | 338 | 339 | 340 | size 341 | 342 | 10.0 343 | 344 | 345 | 346 | allOptionsFieldDescriptorList 347 | 348 | apexColFieldDescriptors_fbc 349 | 350 | 351 | true 352 | 353 | vFieldList 354 | selectedOptionsCSV 355 | 356 | 357 | true 358 | true 359 | 360 | 361 | 270 362 | 49 363 | 364 | Object_Picker 365 | 366 | 367 | Draft 368 | 369 | Run_New_SubFlow 370 | 371 | 951 372 | 203 373 | Datatable_Configuration_Helper_Temp_SubFlow 374 | 375 | vFieldList 376 | 377 | vFieldList 378 | 379 | 380 | 381 | 382 | DatatableConfigFlow 383 | <?xml version="1.0" encoding="UTF-8"?> 384 | <Flow xmlns="http://soap.sforce.com/2006/04/metadata"> 385 | <interviewLabel>Datatable_Configuration_Helper_Temp_SubFlow{!$Flow.CurrentDateTime}</interviewLabel> 386 | <label>Datatable Configuration Helper - Temp SubFlow</label> 387 | <processMetadataValues> 388 | <name>BuilderType</name> 389 | <value> 390 | <stringValue>LightningFlowBuilder</stringValue> 391 | </value> 392 | </processMetadataValues> 393 | <processMetadataValues> 394 | <name>OriginBuilderType</name> 395 | <value> 396 | <stringValue>LightningFlowBuilder</stringValue> 397 | </value> 398 | </processMetadataValues> 399 | <processType>Flow</processType> 400 | <recordLookups> 401 | <name>Get_Records</name> 402 | <label>Get Records</label> 403 | <locationX>269</locationX> 404 | <locationY>196</locationY> 405 | <assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound> 406 | <connector> 407 | <targetReference>Display_Sample_Table</targetReference> 408 | </connector> 409 | <getFirstRecordOnly>false</getFirstRecordOnly> 410 | <object>{!PickObject.objectType}</object> 411 | <storeOutputAutomatically>true</storeOutputAutomatically> 412 | </recordLookups> 413 | <screens> 414 | <name>Display_Sample_Table</name> 415 | <label>Display Sample Table</label> 416 | <locationX>411</locationX> 417 | <locationY>279</locationY> 418 | <allowBack>false</allowBack> 419 | <allowFinish>true</allowFinish> 420 | <allowPause>false</allowPause> 421 | <fields> 422 | <name>Sample</name> 423 | <dataTypeMappings> 424 | <typeName>T</typeName> 425 | <typeValue>{!PickObject.objectType}</typeValue> 426 | </dataTypeMappings> 427 | <extensionName>c:datatableV2</extensionName> 428 | <fieldType>ComponentInstance</fieldType> 429 | <inputParameters> 430 | <name>columnFields</name> 431 | <value> 432 | <elementReference>vFieldList</elementReference> 433 | </value> 434 | </inputParameters> 435 | <inputParameters> 436 | <name>tableData</name> 437 | <value> 438 | <elementReference>Get_Records</elementReference> 439 | </value> 440 | </inputParameters> 441 | <inputParameters> 442 | <name>maxNumberOfRows</name> 443 | <value> 444 | <numberValue>10.0</numberValue> 445 | </value> 446 | </inputParameters> 447 | <inputParameters> 448 | <name>isConfigMode</name> 449 | <value> 450 | <booleanValue>true</booleanValue> 451 | </value> 452 | </inputParameters> 453 | <inputParameters> 454 | <name>columnFilters</name> 455 | <value> 456 | <stringValue>all</stringValue> 457 | </value> 458 | </inputParameters> 459 | <inputParameters> 460 | <name>tableHeight</name> 461 | <value> 462 | <stringValue>27rem</stringValue> 463 | </value> 464 | </inputParameters> 465 | <isRequired>true</isRequired> 466 | <storeOutputAutomatically>true</storeOutputAutomatically> 467 | </fields> 468 | <showFooter>true</showFooter> 469 | <showHeader>true</showHeader> 470 | </screens> 471 | <start> 472 | <locationX>50</locationX> 473 | <locationY>50</locationY> 474 | <connector> 475 | <targetReference>Get_Records</targetReference> 476 | </connector> 477 | </start> 478 | <status>Draft</status> 479 | <variables> 480 | <name>vFieldList</name> 481 | <dataType>String</dataType> 482 | <isCollection>false</isCollection> 483 | <isInput>true</isInput> 484 | <isOutput>false</isOutput> 485 | </variables> 486 | </Flow> 487 | 488 | 489 | apexColFieldDescriptors_fbc 490 | fbc_FieldDescriptor 491 | Apex 492 | true 493 | false 494 | false 495 | 496 | 497 | vFieldList 498 | String 499 | false 500 | false 501 | false 502 | 503 | 504 | -------------------------------------------------------------------------------- /force-app/main/default/flows/Datatable_Configuration_Helper_Temp_SubFlow.flow-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Datatable_Configuration_Helper_Temp_SubFlow7/26/2020 3:37 PM 4 | 5 | 6 | BuilderType 7 | 8 | LightningFlowBuilder 9 | 10 | 11 | 12 | OriginBuilderType 13 | 14 | LightningFlowBuilder 15 | 16 | 17 | Flow 18 | 19 | Get_Records 20 | 21 | 269 22 | 196 23 | false 24 | 25 | Display_Sample_Table 26 | 27 | false 28 | Case 29 | true 30 | 31 | 32 | Display_Sample_Table 33 | 34 | 411 35 | 279 36 | false 37 | true 38 | false 39 | 40 | Sample 41 | 42 | T 43 | Case 44 | 45 | c:datatableV2 46 | ComponentInstance 47 | 48 | columnFields 49 | 50 | vFieldList 51 | 52 | 53 | 54 | tableData 55 | 56 | Get_Records 57 | 58 | 59 | 60 | maxNumberOfRows 61 | 62 | 10.0 63 | 64 | 65 | 66 | isConfigMode 67 | 68 | true 69 | 70 | 71 | 72 | columnFilters 73 | 74 | all 75 | 76 | 77 | 78 | tableHeight 79 | 80 | 27rem 81 | 82 | 83 | true 84 | true 85 | 86 | true 87 | true 88 | 89 | 90 | 50 91 | 50 92 | 93 | Get_Records 94 | 95 | 96 | Draft 97 | 98 | vFieldList 99 | String 100 | false 101 | true 102 | false 103 | 104 | 105 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/datatableV2/datatableV2.html: -------------------------------------------------------------------------------- 1 | 61 | 62 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/datatableV2/datatableV2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lightning Web Component for Flow Screens: datatableV2 3 | * 4 | * VERSION: 2.47 5 | * 6 | * RELEASE NOTES: https://github.com/ericrsmith35/DatatableV2/blob/master/README.md 7 | * 8 | * Copyright (c) 2020, Eric Smith 9 | * 10 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided 11 | * that the following conditions are met: 12 | * 13 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 14 | * 15 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 16 | * in the documentation and/or other materials provided with the distribution. 17 | * 18 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived 19 | * from this software without specific prior written permission. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 22 | * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 23 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | * 28 | **/ 29 | 30 | const VERSION_NUMBER = 2.47; 31 | 32 | import { LightningElement, api, track, wire } from 'lwc'; 33 | import getReturnResults from '@salesforce/apex/SObjectController2.getReturnResults'; 34 | import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; 35 | 36 | const MAXROWCOUNT = 1000; // Limit the total number of records to be handled by this component 37 | const ROUNDWIDTH = 5; // Used to round off the column widths during Config Mode to nearest value 38 | 39 | const MYDOMAIN = 'https://' + window.location.hostname.split('.')[0].replace('--c',''); 40 | 41 | export default class DatatableV2 extends LightningElement { 42 | 43 | // Component Input & Output Attributes 44 | @api tableData = []; 45 | @api columnFields = []; 46 | @api columnAlignments = []; 47 | @api columnCellAttribs = []; 48 | @api columnEdits = ''; 49 | @api columnFilters = ''; 50 | @api columnIcons = []; 51 | @api columnLabels = []; 52 | @api columnOtherAttribs = []; 53 | @api columnTypeAttribs = []; 54 | @api columnWidths = []; 55 | @api keyField = 'Id'; 56 | @api matchCaseOnFilters; 57 | @api maxNumberOfRows; 58 | @api preSelectedRows = []; 59 | @api numberOfRowsSelected = 0; 60 | @api isRequired; 61 | @api isConfigMode; 62 | @api hideCheckboxColumn; 63 | @api singleRowSelection; 64 | @api suppressBottomBar = false; 65 | @api suppressNameFieldLink = false; 66 | @api tableHeight; 67 | @api outputSelectedRows = []; 68 | @api outputSelectedRow; 69 | @api outputEditedRows = []; 70 | @api tableBorder; 71 | @api tableIcon; 72 | @api tableLabel; 73 | 74 | // JSON Version Attributes (User Defined Object) 75 | @api isUserDefinedObject = false; 76 | @api tableDataString = []; 77 | @api preSelectedRowsString = []; 78 | @api outputSelectedRowsString = ''; 79 | @api outputEditedRowsString = ''; 80 | @api columnScales = []; 81 | @api columnTypes = []; 82 | @api scaleAttrib = []; 83 | @api typeAttrib = []; 84 | 85 | // JSON Version Variables 86 | @api scales = []; 87 | @api types = []; 88 | 89 | // Other Datatable attributes 90 | @api sortedBy; 91 | @api sortDirection; 92 | @api maxRowSelection; 93 | @api errors; 94 | @api columnWidthValues; 95 | @track columns = []; 96 | @track mydata = []; 97 | @track selectedRows = []; 98 | @track roundValueLabel; 99 | @track showClearButton = false; 100 | 101 | // Handle Lookup Field Variables 102 | @api lookupId; 103 | @api objectName; 104 | @api objectLinkField; 105 | @track lookupName; 106 | 107 | // Column Filter variables 108 | @api filterColumns; 109 | @api columnFilterValues = []; 110 | @api columnType; 111 | @api columnNumber; 112 | @api baseLabel; 113 | @api isFiltered; 114 | @api saveOriginalValue; 115 | @track columnFilterValue = null; 116 | @track isOpenFilterInput = false; 117 | @track inputLabel; 118 | @track inputType = 'text'; 119 | @track inputFormat = null; 120 | 121 | // Component working variables 122 | @api savePreEditData = []; 123 | @api editedData = []; 124 | @api outputData = []; 125 | @api errorApex; 126 | @api dtableColumnFieldDescriptorString; 127 | @api lookupFieldDataString; 128 | @api basicColumns = []; 129 | @api lookupFieldArray = []; 130 | @api columnArray = []; 131 | @api percentFieldArray = []; 132 | @api noEditFieldArray = []; 133 | @api timeFieldArray = []; 134 | @api picklistFieldArray = []; 135 | @api picklistReplaceValues = false; 136 | @api picklistFieldMap = []; 137 | @api picklistMap = []; 138 | @api edits = []; 139 | @api isEditAttribSet = false; 140 | @api editAttribType = 'none'; 141 | @api filters = []; 142 | @api filterAttribType = 'none'; 143 | @api alignments = []; 144 | @api cellAttribs = []; 145 | @api cellAttributes; 146 | @api icons = []; 147 | @api labels = []; 148 | @api otherAttribs = []; 149 | @api typeAttribs = []; 150 | @api typeAttributes; 151 | @api widths = []; 152 | @api lookups = []; 153 | @api cols = []; 154 | @api attribCount = 0; 155 | @api recordData = []; 156 | @api timezoneOffset = 0; 157 | @track isInvalid = false; 158 | @track isWorking = false; 159 | @track showSpinner = true; 160 | @track borderClass; 161 | @track columnFieldParameter; 162 | @track columnAlignmentParameter; 163 | @track columnLabelParameter; 164 | @track columnWidthParameter; 165 | @track columnEditParameter; 166 | @track columnFilterParameter; 167 | 168 | get formElementClass() { 169 | return this.isInvalid ? 'slds-form-element slds-has-error' : 'slds-form-element'; 170 | } 171 | 172 | get requiredSymbol() { 173 | return this.isRequired ? '*' : ''; 174 | } 175 | 176 | get hasIcon() { 177 | return (this.tableIcon && this.tableIcon.length > 0); 178 | } 179 | 180 | get formattedTableLabel() { 181 | return (this.tableLabel && this.tableLabel.length > 0) ? '

 '+this.tableLabel+'

' : ''; 182 | } 183 | 184 | connectedCallback() { 185 | 186 | console.log("VERSION_NUMBER", VERSION_NUMBER); 187 | 188 | // JSON input attributes 189 | if (this.isUserDefinedObject) { 190 | console.log('tableDataString - ',this.tableDataString); 191 | if (this.tableDataString.length == 0) { 192 | this.tableDataString = '[{"'+this.keyField+'":"(empty table)"}]'; 193 | this.columnFields = this.keyField; 194 | this.columnTypes = []; 195 | this.columnScales = []; 196 | } 197 | this.tableData = JSON.parse(this.tableDataString); 198 | console.log('tableData - ',this.tableData); 199 | this.preSelectedRows = (this.preSelectedRowsString.length > 0) ? JSON.parse(this.preSelectedRowsString) : []; 200 | } 201 | 202 | // Restrict the number of records handled by this component 203 | let min = Math.min(MAXROWCOUNT, this.maxNumberOfRows); 204 | if (this.tableData.length > min) { 205 | this.tableData = [...this.tableData].slice(0,min); 206 | } 207 | 208 | // Set roundValue for setting Column Widths in Config Mode 209 | this.roundValueLabel = "Round to Nearest " + ROUNDWIDTH; 210 | 211 | // Get array of column field API names 212 | this.columnArray = (this.columnFields.length > 0) ? this.columnFields.replace(/\s/g, '').split(',') : []; 213 | this.columnFieldParameter = this.columnArray.join(', '); 214 | console.log('columnArray - ',this.columnArray); 215 | 216 | // JSON Version - Build basicColumns default values 217 | if (this.isUserDefinedObject) { 218 | this.columnArray.forEach(field => { 219 | this.basicColumns.push({ 220 | label: field, 221 | fieldName: field, 222 | type: 'richtext', 223 | scale: 0 224 | }); 225 | }) 226 | } 227 | 228 | // Parse Column Alignment attribute 229 | const parseAlignments = (this.columnAlignments.length > 0) ? this.columnAlignments.replace(/\s/g, '').split(',') : []; 230 | this.attribCount = (parseAlignments.findIndex(f => f.search(':') != -1) != -1) ? 0 : 1; 231 | parseAlignments.forEach(align => { 232 | this.alignments.push({ 233 | column: this.columnReference(align), 234 | alignment: this.columnValue(align) 235 | }); 236 | }); 237 | 238 | // Parse Column Edit attribute 239 | if (this.columnEdits.toLowerCase() != 'all') { 240 | const parseEdits = (this.columnEdits.length > 0) ? this.columnEdits.replace(/\s/g, '').split(',') : []; 241 | this.attribCount = (parseEdits.findIndex(f => f.search(':') != -1) != -1) ? 0 : 1; 242 | this.editAttribType = 'none'; 243 | parseEdits.forEach(edit => { 244 | let colEdit = (this.columnValue(edit).toLowerCase() == 'true') ? true : false; 245 | this.edits.push({ 246 | column: this.columnReference(edit), 247 | edit: colEdit 248 | }); 249 | this.editAttribType = 'cols'; 250 | }); 251 | } else { 252 | this.editAttribType = 'all'; 253 | } 254 | 255 | // Parse Column Filter attribute 256 | if (this.isConfigMode) { 257 | this.columnFilters = ''; 258 | } 259 | if (this.columnFilters.toLowerCase() != 'all') { 260 | const parseFilters = (this.columnFilters.length > 0) ? this.columnFilters.replace(/\s/g, '').split(',') : []; 261 | this.attribCount = (parseFilters.findIndex(f => f.search(':') != -1) != -1) ? 0 : 1; 262 | this.filterAttribType = 'none'; 263 | parseFilters.forEach(filter => { 264 | let colFilter = (this.columnValue(filter).toLowerCase() == 'true') ? true : false; 265 | let col = this.columnReference(filter); 266 | this.filters.push({ 267 | column: col, 268 | filter: colFilter, 269 | actions: [ 270 | {label: 'Set Filter', disabled: false, name: 'filter_' + col, iconName: 'utility:filter'}, 271 | {label: 'Clear Filter', disabled: true, name: 'clear_' + col, iconName: 'utility:clear'} 272 | ] 273 | }); 274 | this.filterAttribType = 'cols'; 275 | }); 276 | } else { 277 | this.filterAttribType = 'all'; 278 | } 279 | 280 | // Parse Column Icon attribute 281 | const parseIcons = (this.columnIcons.length > 0) ? this.columnIcons.replace(/\s/g, '').split(',') : []; 282 | this.attribCount = (parseIcons.findIndex(f => f.search(':') != -1) != -1) ? 0 : 1; 283 | parseIcons.forEach(icon => { 284 | this.icons.push({ 285 | column: this.columnReference(icon), 286 | icon: this.columnValue(icon) 287 | }); 288 | }); 289 | 290 | // Parse Column Label attribute 291 | const parseLabels = (this.columnLabels.length > 0) ? this.removeSpaces(this.columnLabels).split(',') : []; 292 | this.attribCount = (parseLabels.findIndex(f => f.search(':') != -1) != -1) ? 0 : 1; 293 | parseLabels.forEach(label => { 294 | this.labels.push({ 295 | column: this.columnReference(label), 296 | label: this.columnValue(label) 297 | }); 298 | }); 299 | 300 | if (this.isUserDefinedObject) { 301 | 302 | // JSON Version - Parse Column Scale attribute 303 | const parseScales = (this.columnScales.length > 0) ? this.removeSpaces(this.columnScales).split(',') : []; 304 | this.attribCount = (parseScales.findIndex(f => f.search(':') != -1) != -1) ? 0 : 1; 305 | parseScales.forEach(scale => { 306 | this.scales.push({ 307 | column: this.columnReference(scale), 308 | scale: this.columnValue(scale) 309 | }); 310 | this.basicColumns[this.columnReference(scale)].scale = this.columnValue(scale); 311 | }); 312 | 313 | // JSON Version - Parse Column Type attribute 314 | const parseTypes = (this.columnTypes.length > 0) ? this.removeSpaces(this.columnTypes).split(',') : []; 315 | this.attribCount = (parseTypes.findIndex(f => f.search(':') != -1) != -1) ? 0 : 1; 316 | parseTypes.forEach(type => { 317 | this.types.push({ 318 | column: this.columnReference(type), 319 | type: this.columnValue(type) 320 | }); 321 | console.log('*UD type',type); 322 | this.basicColumns[this.columnReference(type)].type = this.columnValue(type); 323 | }); 324 | } 325 | 326 | // Parse Column Width attribute 327 | const parseWidths = (this.columnWidths.length > 0) ? this.columnWidths.replace(/\s/g, '').split(',') : []; 328 | this.attribCount = (parseWidths.findIndex(f => f.search(':') != -1) != -1) ? 0 : 1; 329 | parseWidths.forEach(width => { 330 | this.widths.push({ 331 | column: this.columnReference(width), 332 | width: parseInt(this.columnValue(width)) 333 | }); 334 | }); 335 | 336 | // Parse Column CellAttribute attribute (Because multiple attributes use , these are separated by ;) 337 | const parseCellAttribs = (this.columnCellAttribs.length > 0) ? this.removeSpaces(this.columnCellAttribs).split(';') : []; 338 | this.attribCount = 0; // These attributes must specify a column number or field API name 339 | parseCellAttribs.forEach(cellAttrib => { 340 | this.cellAttribs.push({ 341 | column: this.columnReference(cellAttrib), 342 | attribute: this.columnValue(cellAttrib) 343 | }); 344 | }); 345 | 346 | // Parse Column Other Attributes attribute (Because multiple attributes use , these are separated by ;) 347 | const parseOtherAttribs = (this.columnOtherAttribs.length > 0) ? this.removeSpaces(this.columnOtherAttribs).split(';') : []; 348 | this.attribCount = 0; // These attributes must specify a column number or field API name 349 | parseOtherAttribs.forEach(otherAttrib => { 350 | this.otherAttribs.push({ 351 | column: this.columnReference(otherAttrib), 352 | attribute: this.columnValue(otherAttrib) 353 | }); 354 | }); 355 | 356 | // Parse Column TypeAttribute attribute (Because multiple attributes use , these are separated by ;) 357 | const parseTypeAttribs = (this.columnTypeAttribs.length > 0) ? this.removeSpaces(this.columnTypeAttribs).split(';') : []; 358 | this.attribCount = 0; // These attributes must specify a column number or field API name 359 | parseTypeAttribs.forEach(ta => { 360 | this.typeAttribs.push({ 361 | column: this.columnReference(ta), 362 | attribute: this.columnValue(ta) 363 | }); 364 | }); 365 | 366 | // Set table height 367 | this.tableHeight = 'height:' + this.tableHeight; 368 | console.log('tableHeight',this.tableHeight); 369 | 370 | // Set table border display 371 | this.borderClass = (this.tableBorder != false) ? 'slds-box' : ''; 372 | 373 | // Generate datatable 374 | if (this.tableData) { 375 | 376 | // Set other initial values here 377 | this.maxRowSelection = (this.singleRowSelection) ? 1 : this.tableData.length; 378 | 379 | console.log('Processing Datatable'); 380 | this.processDatatable(); 381 | 382 | } else { 383 | this.showSpinner = false; 384 | } 385 | 386 | // Handle pre-selected records 387 | this.outputSelectedRows = this.preSelectedRows; 388 | this.updateNumberOfRowsSelected(this.outputSelectedRows); 389 | if (this.isUserDefinedObject) { 390 | this.outputSelectedRowsString = JSON.stringify(this.outputSelectedRows); //JSON Version 391 | this.dispatchEvent(new FlowAttributeChangeEvent('outputSelectedRowsString', this.outputSelectedRowsString)); //JSON Version 392 | } else { 393 | this.dispatchEvent(new FlowAttributeChangeEvent('outputSelectedRows', this.outputSelectedRows)); 394 | } 395 | const selected = JSON.parse(JSON.stringify([...this.preSelectedRows])); 396 | selected.forEach(record => { 397 | this.selectedRows.push(record[this.keyField]); 398 | }); 399 | 400 | } 401 | 402 | removeSpaces(string) { 403 | return string 404 | .replace(/, | ,/g,',') 405 | .replace(/: | :/g,':') 406 | .replace(/{ | {/g,'{') 407 | .replace(/} | }/g,'}') 408 | .replace(/; | ;/g,';'); 409 | } 410 | 411 | columnReference(attrib) { 412 | // The column reference can be either the field API name or the column sequence number (1,2,3 ...) 413 | // If no column reference is specified, the values are assigned to columns in order (There must be a value provided for each column) 414 | // Return the actual column # (0,1,2 ...) 415 | let colRef = 0; 416 | if (this.attribCount == 0) { 417 | let colDescriptor = attrib.split(':')[0]; 418 | colRef = Number(colDescriptor)-1; 419 | if (isNaN(colRef)) { 420 | colRef = this.columnArray.indexOf(colDescriptor); 421 | colRef = (colRef != -1) ? colRef : 999; // If no match for the field name, set to non-existent column # 422 | } 423 | } else { 424 | colRef = this.attribCount-1; 425 | this.attribCount += 1; 426 | } 427 | return colRef; 428 | } 429 | 430 | columnValue(attrib) { 431 | // Extract the value from the column attribute 432 | return attrib.slice(attrib.search(':')+1); 433 | } 434 | 435 | processDatatable() { 436 | if (this.isUserDefinedObject) { 437 | 438 | // JSON Version set recordData 439 | this.recordData = [...this.tableData]; 440 | 441 | // JSON Version Special Field Types 442 | this.types.forEach(t => { 443 | switch(t.type) { 444 | case 'percent': 445 | this.percentFieldArray.push(this.basicColumns[t.column].fieldName); 446 | this.basicColumns[t.column].type = 'percent'; 447 | break; 448 | case 'time': 449 | this.timeFieldArray.push(this.basicColumns[t.column].fieldName); 450 | this.basicColumns[t.column].type = 'time'; 451 | break; 452 | case 'lookup': 453 | this.lookupFieldArray.push(this.basicColumns[t.column].fieldName); 454 | this.lookups.push(this.basicColumns[t.column].fieldName); 455 | this.basicColumns[t.column].type = 'lookup'; 456 | break; 457 | case 'richtext': 458 | this.lookupFieldArray.push(this.basicColumns[t.column].fieldName); 459 | this.lookups.push(this.basicColumns[t.column].fieldName); 460 | this.basicColumns[t.column].type = 'richtext'; 461 | } 462 | }); 463 | 464 | // Update row data for lookup, time and percent fields 465 | this.updateDataRows(); 466 | 467 | // Custom column processing 468 | this.updateColumns(); 469 | 470 | if(this.cols[0].fieldName.endsWith('_lookup')) { 471 | this.sortedBy = this.cols[0].fieldName; 472 | this.doSort(this.sortedBy, 'asc'); 473 | } 474 | 475 | // Done processing the datatable 476 | this.showSpinner = false; 477 | 478 | } else { 479 | 480 | // Call Apex Controller and get Column Definitions and update Row Data 481 | let data = (this.tableData) ? JSON.parse(JSON.stringify([...this.tableData])) : []; 482 | let fieldList = (this.columnFields.length > 0) ? this.columnFields.replace(/\s/g, '') : ''; // Remove spaces 483 | console.log('Passing data to Apex Controller', data); 484 | getReturnResults({ records: data, fieldNames: fieldList }) 485 | .then(result => { 486 | let returnResults = JSON.parse(result); 487 | 488 | // Assign return results from the Apex callout 489 | this.recordData = [...returnResults.rowData]; 490 | this.lookups = returnResults.lookupFieldList; 491 | this.percentFieldArray = (returnResults.percentFieldList.length > 0) ? returnResults.percentFieldList.toString().split(',') : []; 492 | this.timeFieldArray = (returnResults.timeFieldList.length > 0) ? returnResults.timeFieldList.toString().split(',') : []; 493 | this.picklistFieldArray = (returnResults.picklistFieldList.length > 0) ? returnResults.picklistFieldList.toString().split(',') : []; 494 | this.picklistReplaceValues = (this.picklistFieldArray.length > 0); // Flag value dependent on if there are any picklists in the datatable field list 495 | this.picklistFieldMap = returnResults.picklistFieldMap; 496 | this.objectName = returnResults.objectName; 497 | this.objectLinkField = returnResults.objectLinkField; 498 | this.lookupFieldArray = JSON.parse('[' + returnResults.lookupFieldData + ']'); 499 | this.timezoneOffset = returnResults.timezoneOffset.replace(/,/g, ''); 500 | 501 | // Check for differences in picklist API Values vs Labels 502 | if (this.picklistReplaceValues) { 503 | let noMatch = false; 504 | this.picklistFieldArray.forEach(picklist => { 505 | Object.keys(this.picklistFieldMap[picklist]).forEach(map => { 506 | if (map != this.picklistFieldMap[picklist][map]) { 507 | noMatch = true; 508 | } 509 | }); 510 | }); 511 | this.picklistReplaceValues = noMatch; 512 | } 513 | 514 | // Basic column info (label, fieldName, type) taken from the Schema in Apex 515 | this.dtableColumnFieldDescriptorString = '[' + returnResults.dtableColumnFieldDescriptorString + ']'; 516 | this.basicColumns = JSON.parse(this.dtableColumnFieldDescriptorString); 517 | console.log('dtableColumnFieldDescriptorString',this.dtableColumnFieldDescriptorString,this.basicColumns); 518 | this.noEditFieldArray = (returnResults.noEditFieldList.length > 0) ? returnResults.noEditFieldList.toString().split(',') : []; 519 | 520 | // Update row data for lookup, time, picklist and percent fields 521 | this.updateDataRows(); 522 | 523 | // Custom column processing 524 | this.updateColumns(); 525 | 526 | // Done processing the datatable 527 | this.showSpinner = false; 528 | 529 | }) // Handle any errors from the Apex Class 530 | .catch(error => { 531 | console.log('getReturnResults error is: ' + JSON.stringify(error)); 532 | this.errorApex = 'Apex Action error: ' + error.body.message; 533 | alert(this.errorApex + '\n' + error.body.stackTrace); // Present the error to the user 534 | this.showSpinner = false; 535 | return this.errorApex; 536 | }); 537 | 538 | } 539 | 540 | } 541 | 542 | updateDataRows() { 543 | // Process Incoming Data Collection 544 | let data = (this.recordData) ? JSON.parse(JSON.stringify([...this.recordData])) : []; 545 | let lookupFields = this.lookups; 546 | let lufield = ''; 547 | let timeFields = this.timeFieldArray; 548 | let percentFields = this.percentFieldArray; 549 | let picklistFields = this.picklistFieldArray; 550 | let lookupFieldObject = ''; 551 | 552 | data.forEach(record => { 553 | 554 | // Prepend a date to the Time field so it can be displayed and calculate offset based on User's timezone 555 | timeFields.forEach(time => { 556 | if (record[time]) { 557 | record[time] = "2020-05-12T" + record[time]; 558 | let dt = Date.parse(record[time]); 559 | let d = new Date(); 560 | record[time] = d.setTime(Number(dt) - Number(this.timezoneOffset)); 561 | } 562 | }); 563 | 564 | // Store percent field data as value/100 565 | percentFields.forEach(pct => { 566 | record[pct] = record[pct]/100; 567 | }); 568 | 569 | // Flatten returned data 570 | lookupFields.forEach(lookup => { 571 | if (this.isUserDefinedObject) { 572 | lufield = lookup; 573 | record[lufield + '_lookup'] = MYDOMAIN + record[lufield + '_lookup']; 574 | } else { 575 | if(lookup.toLowerCase().endsWith('id')) { 576 | lufield = lookup.replace(/Id$/gi,''); 577 | } else { 578 | lufield = lookup.replace(/__c$/gi,'__r'); 579 | } 580 | 581 | // Get the lookup field details 582 | lookupFieldObject = this.lookupFieldArray.filter(obj => Object.keys(obj).some(key => obj[key].includes(lufield)))[0]; 583 | 584 | if (record[lufield]) { 585 | record[lufield + '_name'] = record[lufield][lookupFieldObject['nameField']]; 586 | record[lufield + '_id'] = record[lufield]['Id']; 587 | // Add new column with correct Lookup urls 588 | record[lufield + '_lookup'] = MYDOMAIN + '.lightning.force.com/lightning/r/' + lookupFieldObject['object'] + '/' + record[lufield + '_id'] + '/view'; 589 | } 590 | } 591 | }); 592 | 593 | // Handle Lookup for the SObject's "Name" Field 594 | record[this.objectLinkField + '_name'] = record[this.objectLinkField]; 595 | record[this.objectLinkField + '_lookup'] = MYDOMAIN + '.lightning.force.com/lightning/r/' + this.objectName + '/' + record['Id'] + '/view'; 596 | 597 | // Handle replacement of Picklist API Names with Labels 598 | if (this.picklistReplaceValues) { 599 | picklistFields.forEach(picklist => { 600 | if (record[picklist]) { 601 | let picklistLabels = []; 602 | record[picklist].split(';').forEach(picklistValue => { 603 | picklistLabels.push(this.picklistFieldMap[picklist][picklistValue]); 604 | }); 605 | record[picklist] = picklistLabels.join(';'); 606 | } 607 | }); 608 | } 609 | 610 | // If needed, add more fields to datatable records 611 | // (Useful for Custom Row Actions/Buttons) 612 | // record['addField'] = 'newValue'; 613 | 614 | }); 615 | 616 | // Set table data attributes 617 | this.mydata = [...data]; 618 | this.savePreEditData = [...this.mydata]; 619 | this.editedData = JSON.parse(JSON.stringify([...this.tableData])); // Must clone because cached items are read-only 620 | console.log('selectedRows',this.selectedRows); 621 | console.log('keyField:',this.keyField); 622 | console.log('tableData',this.tableData); 623 | console.log('mydata:',this.mydata); 624 | } 625 | 626 | updateColumns() { 627 | // Parse column definitions 628 | this.cols = []; 629 | let columnNumber = 0; 630 | let lufield = ''; 631 | 632 | this.basicColumns.forEach(colDef => { 633 | 634 | // Standard parameters 635 | let label = colDef['label']; 636 | let fieldName = colDef['fieldName']; 637 | let type = colDef['type']; 638 | let scale = colDef['scale']; 639 | this.cellAttributes = {}; 640 | this.typeAttributes = {}; 641 | let editAttrib = []; 642 | let filterAttrib = []; 643 | let widthAttrib = []; 644 | this.typeAttrib.type = type; 645 | 646 | // Update Alignment attribute overrides by column 647 | let alignmentAttrib = this.alignments.find(i => i['column'] == columnNumber); 648 | if (alignmentAttrib) { 649 | let alignment = alignmentAttrib.alignment.toLowerCase(); 650 | switch (alignment) { 651 | case 'left': 652 | case 'center': 653 | case 'right': 654 | break; 655 | default: 656 | alignment = 'left'; 657 | } 658 | this.cellAttributes = { alignment:alignment }; 659 | } 660 | 661 | // Update Edit attribute overrides by column 662 | switch (this.editAttribType) { 663 | case 'cols': 664 | editAttrib = this.edits.find(i => i['column'] == columnNumber); 665 | break; 666 | case 'all': 667 | editAttrib.edit = true; 668 | break; 669 | default: 670 | editAttrib.edit = false; 671 | } 672 | 673 | // Some data types are not editable 674 | if(editAttrib) { 675 | switch (type) { 676 | case 'location': 677 | case 'lookup': 678 | case 'time': 679 | editAttrib.edit = false; 680 | break; 681 | case 'text': 682 | if (this.noEditFieldArray.indexOf(fieldName) != -1) editAttrib.edit = false; 683 | break; 684 | default: 685 | } 686 | } 687 | 688 | // Update Filter attribute overrides by column 689 | if (this.isConfigMode) { 690 | filterAttrib.column = columnNumber; 691 | filterAttrib.filter = true; 692 | filterAttrib.actions = [ 693 | {label: 'Align Left', checked: (this.convertType(type) != 'number'), name: 'alignl_' + columnNumber, iconName: 'utility:left_align_text'}, 694 | {label: 'Align Center', checked: false, name: 'alignc_' + columnNumber, iconName: 'utility:center_align_text'}, 695 | {label: 'Align Right', checked: (this.convertType(type) == 'number'), name: 'alignr_' + columnNumber, iconName: 'utility:right_align_text'}, 696 | {label: 'Change Label', disabled: false, name: 'label_' + columnNumber, iconName: 'utility:text'}, 697 | {label: 'Cancel Change', disabled: true, name: 'clear_' + columnNumber, iconName: 'utility:clear'}, 698 | {label: 'Allow Edit', checked: false, name: 'aedit_' + columnNumber, iconName: 'utility:edit'}, 699 | {label: 'Allow Filter', checked: false, name: 'afilter_' + columnNumber, iconName: 'utility:filter'} 700 | ]; 701 | let configAlign = (this.convertType(type) != 'number') ? 'left' : 'right'; 702 | this.cellAttributes = { alignment: configAlign }; 703 | 704 | } else { 705 | switch (this.filterAttribType) { 706 | case 'cols': 707 | filterAttrib = this.filters.find(i => i['column'] == columnNumber); 708 | if (!filterAttrib) { 709 | filterAttrib = []; 710 | filterAttrib.filter = false; 711 | } 712 | break; 713 | case 'all': 714 | filterAttrib.column = columnNumber; 715 | filterAttrib.filter = true; 716 | filterAttrib.actions = [ 717 | {label: 'Set Filter', disabled: false, name: 'filter_' + columnNumber, iconName: 'utility:filter'}, 718 | {label: 'Clear Filter', disabled: true, name: 'clear_' + columnNumber, iconName: 'utility:clear'} 719 | ]; 720 | break; 721 | default: 722 | filterAttrib.filter = false; 723 | } 724 | 725 | // Some data types are not filterable 726 | if(filterAttrib) { 727 | switch (type) { 728 | case 'location': 729 | filterAttrib.filter = false; 730 | break; 731 | default: 732 | } 733 | } 734 | } 735 | 736 | // Update Icon attribute overrides by column 737 | let iconAttrib = this.icons.find(i => i['column'] == columnNumber); 738 | 739 | // Update Label attribute overrides by column 740 | let labelAttrib = this.labels.find(i => i['column'] == columnNumber); 741 | 742 | if (this.isUserDefinedObject) { 743 | // JSON Version - Update Scale attribute overrides by column 744 | this.scaleAttrib = this.scales.find(i => i['column'] == columnNumber); 745 | if (!this.scaleAttrib) { 746 | this.scaleAttrib = []; 747 | this.scaleAttrib.scale = scale; 748 | } 749 | 750 | // JSON Version - Update Type attribute overrides by column 751 | if(type != 'lookup') { 752 | this.typeAttrib = this.types.find(i => i['column'] == columnNumber); 753 | if (!this.typeAttrib) { 754 | this.typeAttrib = []; 755 | this.typeAttrib.type = type; 756 | } 757 | } 758 | } 759 | 760 | // Update Width attribute overrides by column 761 | widthAttrib = this.widths.find(i => i['column'] == columnNumber); 762 | 763 | // Set default typeAttributes based on data type 764 | switch(type) { 765 | case 'date': 766 | case 'date-local': 767 | this.typeAttributes = { year:'numeric', month:'numeric', day:'numeric' } 768 | break; 769 | case 'datetime': 770 | type = 'date'; 771 | this.typeAttrib.type = type; 772 | this.typeAttributes = { year:'numeric', month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit', timeZoneName:'short' }; 773 | break; 774 | case 'time': 775 | type = 'date'; 776 | this.typeAttrib.type = type; 777 | this.typeAttributes = { hour:'2-digit', minute:'2-digit', timeZoneName:'short' }; 778 | break; 779 | case 'currency': 780 | case 'number': 781 | case 'percent': 782 | if (this.isUserDefinedObject) { 783 | let minDigits = (this.scaleAttrib) ? this.scaleAttrib.scale : scale; 784 | this.typeAttributes = { minimumFractionDigits: minDigits }; // JSON Version 785 | } else { 786 | this.typeAttributes = { minimumFractionDigits:scale }; // Show the number of decimal places defined for the field 787 | } 788 | break; 789 | case 'richtext': 790 | this.typeAttrib.type = 'richtext'; 791 | break; 792 | default: 793 | 794 | } 795 | 796 | // Change lookup to url and reference the new fields that will be added to the datatable object 797 | if(type == 'lookup') { 798 | if(this.lookups.includes(fieldName)) { 799 | this.typeAttrib.type = 'url'; 800 | if(fieldName.toLowerCase().endsWith('id')) { 801 | lufield = fieldName.replace(/Id$/gi,''); 802 | } else { 803 | lufield = fieldName.replace(/__c$/gi,'__r'); 804 | } 805 | fieldName = lufield + '_lookup'; 806 | this.typeAttributes = { label: { fieldName: lufield + '_name' }, target: '_blank' }; 807 | } else { 808 | this.typeAttrib.type = 'text'; // Non reparentable Master-Detail fields are not supported 809 | } 810 | } 811 | 812 | // Switch the SObject's "Name" Field to a Lookup 813 | if (fieldName == this.objectLinkField && !this.suppressNameFieldLink) { 814 | this.typeAttrib.type = 'url'; 815 | fieldName = fieldName + '_lookup'; 816 | this.typeAttributes = { label: { fieldName: this.objectLinkField }, target: '_blank' }; 817 | this.cellAttributes.wrapText = true; 818 | } 819 | 820 | // Update CellAttribute attribute overrides by column 821 | this.parseAttributes('cell',this.cellAttribs,columnNumber); 822 | 823 | // Update TypeAttribute attribute overrides by column 824 | this.parseAttributes('type',this.typeAttribs,columnNumber); 825 | 826 | // Save the updated column definitions 827 | this.cols.push({ 828 | label: (labelAttrib) ? labelAttrib.label : label, 829 | iconName: (iconAttrib) ? iconAttrib.icon : null, 830 | fieldName: fieldName, 831 | type: this.typeAttrib.type, 832 | cellAttributes: this.cellAttributes, 833 | typeAttributes: this.typeAttributes, 834 | editable: (editAttrib) ? editAttrib.edit : false, 835 | actions: (filterAttrib.filter) ? filterAttrib.actions : null, 836 | sortable: 'true', 837 | initialWidth: (widthAttrib) ? widthAttrib.width : null 838 | }); 839 | console.log('this.cols',this.cols); 840 | 841 | // Update Other Attributes attribute overrides by column 842 | this.parseAttributes('other',this.otherAttribs,columnNumber); 843 | 844 | // Repeat for next column 845 | columnNumber += 1; 846 | }); 847 | this.columns = this.cols; 848 | 849 | } 850 | 851 | parseAttributes(propertyType,inputAttributes,columnNumber) { 852 | // Parse regular and nested name:value attribute pairs 853 | let result = []; 854 | let fullAttrib = inputAttributes.find(i => i['column'] == columnNumber); 855 | if (fullAttrib) { 856 | let attribSplit = this.removeSpaces(fullAttrib.attribute.slice(1,-1)).split(','); 857 | attribSplit.forEach(ca => { 858 | let subAttribPos = ca.search('{'); 859 | if (subAttribPos != -1) { 860 | // This attribute value has another attribute object definition {name: {name:value}} 861 | let value = {}; 862 | let name = ca.split(':')[0]; 863 | let attrib = ca.slice(subAttribPos).slice(1,-1); 864 | let rightName = attrib.split(':')[0]; 865 | let rightValue = attrib.slice(attrib.search(':')+1); 866 | value[rightName] = rightValue.replace(/["']{1}/gi,""); // Remove single or double quotes 867 | result['name'] = name; 868 | result['value'] = value; 869 | } else { 870 | // This is a standard attribute definition {name:value} 871 | let attrib = ca.split(':'); 872 | result['name'] = attrib[0]; 873 | attrib.shift(); 874 | result['value'] = attrib.join(':').replace(/["']{1}/gi,""); // Remove single or double quotes; 875 | } 876 | 877 | switch(propertyType) { 878 | case 'cell': 879 | this.cellAttributes[result['name']] = result['value']; 880 | break; 881 | case 'type': 882 | this.typeAttributes[result['name']] = result['value']; 883 | break; 884 | default: // 'other' 885 | this.cols[columnNumber][result['name']] = result['value']; 886 | } 887 | }); 888 | } 889 | } 890 | 891 | handleRowAction(event) { 892 | // Process the row actions here 893 | const action = event.detail.action; 894 | const row = JSON.parse(JSON.stringify(event.detail.row)); 895 | const keyValue = row[this.keyField]; 896 | this.mydata = this.mydata.map(rowData => { 897 | if (rowData[this.keyField] === keyValue) { 898 | switch (action.name) { 899 | // case 'action': goes here 900 | // 901 | // break; 902 | default: 903 | } 904 | } 905 | return rowData; 906 | }); 907 | } 908 | 909 | handleCellChange(event) { 910 | // If suppressBottomBar is false, wait for the Save or Cancel button 911 | if (this.suppressBottomBar) { 912 | this.handleSave(event); 913 | } 914 | } 915 | 916 | handleSave(event) { 917 | // Only used with inline editing 918 | const draftValues = event.detail.draftValues; 919 | 920 | // Apply drafts to mydata 921 | let data = [...this.mydata]; 922 | data = data.map(item => { 923 | const draft = draftValues.find(d => d[this.keyField] == item[this.keyField]); 924 | if (draft != undefined) { 925 | let fieldNames = Object.keys(draft); 926 | fieldNames.forEach(el => item[el] = draft[el]); 927 | } 928 | return item; 929 | }); 930 | 931 | // Apply drafts to editedData 932 | let edata = [...this.editedData]; 933 | edata = edata.map(eitem => { 934 | const edraft = draftValues.find(d => d[this.keyField] == eitem[this.keyField]); 935 | if (edraft != undefined) { 936 | let efieldNames = Object.keys(edraft); 937 | efieldNames.forEach(ef => { 938 | // if(this.percentFieldArray.indexOf(ef) != -1) { 939 | // eitem[ef] = Number(edraft[ef])*100; // Percent field 940 | // } 941 | eitem[ef] = edraft[ef]; 942 | }); 943 | 944 | // Add/update edited record to output collection 945 | const orecord = this.outputEditedRows.find(o => o[this.keyField] == eitem[this.keyField]); // Check to see if already in output collection 946 | if (orecord) { 947 | const otherEditedRows = []; 948 | this.outputEditedRows.forEach(er => { // Remove current row so it can be replaced with the new edits 949 | if (er[this.keyField] != eitem[this.keyField]) { 950 | otherEditedRows.push(er); 951 | } 952 | }); 953 | this.outputEditedRows = [...otherEditedRows]; 954 | } 955 | this.outputEditedRows = [...this.outputEditedRows,eitem]; // Add to output attribute collection 956 | if (this.isUserDefinedObject) { 957 | this.outputEditedRowsString = JSON.stringify(this.outputEditedRows); //JSON Version 958 | this.dispatchEvent(new FlowAttributeChangeEvent('outputEditedRowsString', this.outputEditedRowsString)); //JSON Version 959 | } else { 960 | this.dispatchEvent(new FlowAttributeChangeEvent('outputEditedRows', this.outputEditedRows)); 961 | } 962 | } 963 | return eitem; 964 | }); 965 | 966 | this.savePreEditData = [...data]; // Resave the current table values 967 | this.mydata = [...data]; // Reset the current table values 968 | if (!this.suppressBottomBar) { 969 | this.columns = [...this.columns]; // Force clearing of the edit highlights 970 | } 971 | } 972 | 973 | cancelChanges(event) { 974 | // Only used with inline editing 975 | this.mydata = [...this.savePreEditData]; 976 | } 977 | 978 | handleRowSelection(event) { 979 | // Only used with row selection 980 | // Update values to be passed back to the Flow 981 | let currentSelectedRows = event.detail.selectedRows; 982 | this.updateNumberOfRowsSelected(currentSelectedRows); 983 | this.setIsInvalidFlag(false); 984 | if(this.isRequired && this.numberOfRowsSelected == 0) { 985 | this.setIsInvalidFlag(true); 986 | } 987 | let sdata = []; 988 | currentSelectedRows.forEach(srow => { 989 | const selData = this.tableData.find(d => d[this.keyField] == srow[this.keyField]); 990 | sdata.push(selData); 991 | }); 992 | this.outputSelectedRows = [...sdata]; // Set output attribute values 993 | if (this.isUserDefinedObject) { 994 | this.outputSelectedRowsString = JSON.stringify(this.outputSelectedRows); //JSON Version 995 | this.dispatchEvent(new FlowAttributeChangeEvent('outputSelectedRowsString', this.outputSelectedRowsString)); //JSON Version 996 | } else { 997 | this.dispatchEvent(new FlowAttributeChangeEvent('outputSelectedRows', this.outputSelectedRows)); 998 | } 999 | console.log('outputSelectedRows',this.outputSelectedRows); 1000 | } 1001 | 1002 | updateNumberOfRowsSelected(currentSelectedRows) { 1003 | // Handle updating output attribute for the number of selected rows 1004 | this.numberOfRowsSelected = currentSelectedRows.length; 1005 | this.dispatchEvent(new FlowAttributeChangeEvent('numberOfRowsSelected', this.numberOfRowsSelected)); 1006 | // Return an SObject Record if just a single row is selected 1007 | this.outputSelectedRow = (this.numberOfRowsSelected == 1) ? currentSelectedRows[0] : null; 1008 | this.dispatchEvent(new FlowAttributeChangeEvent('outputSelectedRow', this.outputSelectedRow)); 1009 | this.showClearButton = (this.tableData.length == 1 && this.numberOfRowsSelected == 1); 1010 | } 1011 | 1012 | handleClearSelection() { 1013 | this.showClearButton = false; 1014 | this.selectedRows = []; 1015 | this.outputSelectedRows = this.selectedRows; 1016 | this.dispatchEvent(new FlowAttributeChangeEvent('outputSelectedRows', this.outputSelectedRows)); 1017 | } 1018 | 1019 | updateColumnSorting(event) { 1020 | // Handle column sorting 1021 | console.log('Sort:',event.detail.fieldName,event.detail.sortDirection); 1022 | this.sortedBy = event.detail.fieldName; 1023 | this.sortedDirection = event.detail.sortDirection; 1024 | this.doSort(this.sortedBy, this.sortedDirection); 1025 | } 1026 | 1027 | doSort(sortField, sortDirection) { 1028 | // Change sort field from Id to Name for lookups 1029 | if (sortField.endsWith('_lookup')) { 1030 | sortField = sortField.slice(0,sortField.lastIndexOf('_lookup')) + '_name'; 1031 | } 1032 | let fieldValue = row => row[sortField] || ''; 1033 | let reverse = sortDirection === 'asc'? 1: -1; 1034 | 1035 | this.isWorking = true; 1036 | new Promise((resolve, reject) => { 1037 | setTimeout(() => { 1038 | this.mydata = [...this.mydata.sort( 1039 | (a,b)=>(a=fieldValue(a),b=fieldValue(b),reverse*((a>b)-(b>a))) 1040 | )]; 1041 | resolve(); 1042 | }, 0); 1043 | }) 1044 | .then( 1045 | () => this.isWorking = false 1046 | ); 1047 | } 1048 | 1049 | handleHeaderAction(event) { 1050 | // Handle Set Filter and Clear Filter 1051 | const actionName = event.detail.action.name; 1052 | if (actionName.substr(actionName.length - 4) == 'Text') { // Exit if system header action of wrapText or clipText 1053 | return; 1054 | } 1055 | this.isFiltered = false; 1056 | const colDef = event.detail.columnDefinition; 1057 | this.filterColumns = JSON.parse(JSON.stringify([...this.columns])); 1058 | this.columnNumber = Number(actionName.split("_")[1]); 1059 | this.baseLabel = this.filterColumns[this.columnNumber].label.split(' [')[0]; 1060 | const prompt = (this.isConfigMode) ? 'Label' : 'Filter'; 1061 | this.inputLabel = 'Column ' + prompt + ': ' + this.baseLabel; 1062 | switch(actionName.split('_')[0]) { 1063 | 1064 | case 'alignl': // Config Mode Only 1065 | this.filterColumns[this.columnNumber].cellAttributes = {alignment: 'left'}; 1066 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignl_'+this.columnNumber).checked = true; 1067 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignc_'+this.columnNumber).checked = false; 1068 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignr_'+this.columnNumber).checked = false; 1069 | this.columns = [...this.filterColumns]; 1070 | this.updateAlignmentParam(); 1071 | break; 1072 | 1073 | case 'alignc': // Config Mode Only 1074 | this.filterColumns[this.columnNumber].cellAttributes = {alignment: 'center'}; 1075 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignl_'+this.columnNumber).checked = false; 1076 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignc_'+this.columnNumber).checked = true; 1077 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignr_'+this.columnNumber).checked = false; 1078 | this.columns = [...this.filterColumns]; 1079 | this.updateAlignmentParam(); 1080 | break; 1081 | 1082 | case 'alignr': // Config Mode Only 1083 | this.filterColumns[this.columnNumber].cellAttributes = {alignment: 'right'}; 1084 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignl_'+this.columnNumber).checked = false; 1085 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignc_'+this.columnNumber).checked = false; 1086 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'alignr_'+this.columnNumber).checked = true; 1087 | this.columns = [...this.filterColumns]; 1088 | this.updateAlignmentParam(); 1089 | break; 1090 | 1091 | case 'aedit': // Config Mode Only 1092 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'aedit_'+this.columnNumber).checked ^= true; // Flip True-False Value 1093 | this.columns = [...this.filterColumns]; 1094 | this.updateEditParam(); 1095 | break; 1096 | 1097 | case 'afilter': // Config Mode Only 1098 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'afilter_'+this.columnNumber).checked ^= true; // Flip True-False Value 1099 | this.columns = [...this.filterColumns]; 1100 | this.updateFilterParam(); 1101 | break; 1102 | 1103 | case 'label': // Config Mode Only 1104 | this.columnFilterValue = this.columnFilterValues[this.columnNumber]; 1105 | this.columnFilterValue = (this.columnFilterValue) ? this.columnFilterValue : this.baseLabel; 1106 | this.columnType = 'richtext'; 1107 | this.inputType = this.convertType(this.columnType); 1108 | this.inputFormat = (this.inputType == 'number') ? this.convertFormat(this.columnType) : null; 1109 | this.handleOpenFilterInput(); 1110 | break; 1111 | 1112 | case 'filter': 1113 | this.columnFilterValue = this.columnFilterValues[this.columnNumber]; 1114 | this.columnFilterValue = (this.columnFilterValue) ? this.columnFilterValue : null; 1115 | this.columnType = colDef.type; 1116 | this.inputType = this.convertType(this.columnType); 1117 | this.inputFormat = (this.inputType == 'number') ? this.convertFormat(this.columnType) : null; 1118 | this.handleOpenFilterInput(); 1119 | break; 1120 | 1121 | case 'clear': 1122 | this.filterColumns[this.columnNumber].label = (this.isConfigMode) ? this.cols[this.columnNumber].label : this.baseLabel; 1123 | this.columnFilterValue = null; 1124 | this.columnFilterValues[this.columnNumber] = this.columnFilterValue; 1125 | if (this.isConfigMode) { 1126 | this.updateLabelParam(); 1127 | } 1128 | 1129 | this.isWorking = true; 1130 | new Promise((resolve, reject) => { 1131 | setTimeout(() => { 1132 | this.filterColumnData(); 1133 | resolve(); 1134 | }, 0); 1135 | }) 1136 | .then( 1137 | () => this.isWorking = false 1138 | ); 1139 | 1140 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'clear_'+this.columnNumber).disabled = true; 1141 | if (this.sortedBy != undefined) { 1142 | this.doSort(this.sortedBy, this.sortedDirection); // Re-Sort the data 1143 | } 1144 | break; 1145 | 1146 | default: 1147 | } 1148 | 1149 | this.columns = [...this.filterColumns]; 1150 | } 1151 | 1152 | convertType(colType) { 1153 | // Set Input Type based on column Data Type 1154 | switch(colType) { 1155 | case 'boolean': 1156 | return 'text'; 1157 | case 'date': 1158 | return 'date'; 1159 | case 'date-local': 1160 | return 'date'; 1161 | case 'datetime': 1162 | return 'datetime'; 1163 | case 'time': 1164 | return 'time'; 1165 | case 'email': 1166 | return 'email'; 1167 | case 'phone': 1168 | return 'tel'; 1169 | case 'url': 1170 | return 'url'; 1171 | case 'number': 1172 | return 'number'; 1173 | case 'currency': 1174 | return 'number'; 1175 | case 'percent': 1176 | return 'number'; 1177 | case 'text': 1178 | return 'text'; 1179 | default: 1180 | return 'richtext'; 1181 | } 1182 | } 1183 | 1184 | convertFormat(colType) { 1185 | // Set Input Formatter value for different number types 1186 | switch(colType) { 1187 | case 'currency': 1188 | return 'currency'; 1189 | case 'percent': 1190 | // return 'percent-fixed'; // This would be to enter 35 to get 35% (0.35) 1191 | return 'percent'; 1192 | default: 1193 | return null; 1194 | } 1195 | } 1196 | 1197 | handleResize(event) { 1198 | // Save the current column widths and update the config parameter 1199 | this.columnWidthValues = event.detail.columnWidths; 1200 | this.setWidth(this.columnWidthValues); 1201 | } 1202 | 1203 | handleRoundWidths() { 1204 | // Round the Width values to the nearest ROUNDWIDTH 1205 | let widths = []; 1206 | this.columnWidthValues.forEach(w => { 1207 | widths.push(Math.round(w/ROUNDWIDTH)*ROUNDWIDTH); 1208 | }); 1209 | this.setWidth(widths); 1210 | this.columns = [...this.columns]; 1211 | } 1212 | 1213 | setWidth(sizes) { 1214 | // Update the column width values and the Config Mode parameter 1215 | var colNum = 0; 1216 | var colString = ''; 1217 | this.basicColumns.forEach(colDef => { 1218 | this.columns[colNum]['initialWidth'] = sizes[colNum]; 1219 | if (this.filterColumns) { 1220 | this.filterColumns[colNum]['initialWidth'] = sizes[colNum]; 1221 | } 1222 | colString = colString + ', ' + colDef['fieldName'] + ':' + sizes[colNum]; 1223 | colNum += 1; 1224 | }); 1225 | this.columnWidthParameter = colString.substring(2); 1226 | } 1227 | 1228 | handleChange(event) { 1229 | // Update the filter value as the user types it in 1230 | this.columnFilterValue = event.target.value; 1231 | this.columnFilterValues[this.columnNumber] = this.columnFilterValue; 1232 | this.isFiltered = false; 1233 | } 1234 | 1235 | handleSelectAllEdit(event) { 1236 | // Set the Allow Edit Value to True for All Columns 1237 | this.filterColumns = JSON.parse(JSON.stringify([...this.columns])); 1238 | var colNum = 0; 1239 | this.filterColumns.forEach(colDef => { 1240 | colDef['actions'].find(a => a.name == 'aedit_'+colNum).checked = true; 1241 | colNum += 1; 1242 | }); 1243 | this.columnEditParameter = 'All'; 1244 | this.columns = [...this.filterColumns]; 1245 | } 1246 | 1247 | handleSelectAllFilter(event) { 1248 | // Set the Allow Edit Value to True for All Columns 1249 | this.filterColumns = JSON.parse(JSON.stringify([...this.columns])); 1250 | var colNum = 0; 1251 | this.filterColumns.forEach(colDef => { 1252 | colDef['actions'].find(a => a.name == 'afilter_'+colNum).checked = true; 1253 | colNum += 1; 1254 | }); 1255 | this.columnFilterParameter = 'All'; 1256 | this.columns = [...this.filterColumns]; 1257 | } 1258 | 1259 | handleOpenFilterInput() { 1260 | // Display the input dialog for the filter value 1261 | this.saveOriginalValue = this.columnFilterValue; 1262 | this.isOpenFilterInput = true; 1263 | } 1264 | 1265 | handleCommit() { 1266 | // Handle the filter input when the user clicks out of the input dialog 1267 | if (this.columnFilterValue != null) { 1268 | this.handleCloseFilterInput(); 1269 | } 1270 | } 1271 | 1272 | handleCloseFilterInput() { 1273 | // Close the input dialog and handle the new column filter value 1274 | this.isOpenFilterInput = false; 1275 | if (this.columnType == 'boolean') { 1276 | var firstChar = this.columnFilterValue.substring(0, 1).toLowerCase(); 1277 | if (firstChar == 't' || firstChar == 'y' || firstChar == '1') { // True, Yes, 1 - allow multiple ways to select a True value 1278 | this.columnFilterValue = 'true'; 1279 | } else { 1280 | this.columnFilterValue = 'false'; 1281 | } 1282 | this.columnFilterValues[this.columnNumber] = this.columnFilterValue; 1283 | } 1284 | 1285 | if (!this.isFiltered) this.filterColumnData(); 1286 | 1287 | if (this.isConfigMode) { 1288 | this.filterColumns[this.columnNumber].label = this.columnFilterValue; 1289 | this.updateLabelParam(); 1290 | } else { 1291 | this.filterColumns[this.columnNumber].label = this.baseLabel + ' [' + this.columnFilterValue + ']'; 1292 | } 1293 | this.columnFilterValues[this.columnNumber] = this.columnFilterValue; 1294 | // Force a redisplay of the datatable with the filter value shown in the column header 1295 | this.columns = [...this.filterColumns]; 1296 | } 1297 | 1298 | handleCloseModal() { 1299 | // Close the input dialog and cancel any changes 1300 | this.columnFilterValue = this.saveOriginalValue; 1301 | this.columnFilterValues[this.columnNumber] = this.columnFilterValue; 1302 | this.isOpenFilterInput = false; 1303 | } 1304 | 1305 | filterColumnData() { 1306 | // Filter the rows based on the current column filter values 1307 | this.isWorking = true; 1308 | new Promise((resolve, reject) => { 1309 | setTimeout(() => { 1310 | if (!this.isConfigMode) { 1311 | const rows = [...this.savePreEditData]; 1312 | const cols = this.columnFilterValues; 1313 | var filteredRows = []; 1314 | rows.forEach(row => { 1315 | var match = true; 1316 | for (var col = 0; col < cols.length; col++) { 1317 | var fieldName = this.filterColumns[col].fieldName; 1318 | if (fieldName.endsWith('_lookup')) { 1319 | fieldName = fieldName.slice(0,fieldName.lastIndexOf('_lookup')) + '_name'; 1320 | } 1321 | if (this.columnFilterValues[col] && this.columnFilterValues[col] != null) { 1322 | if (!row[fieldName] || row[fieldName] == null) { // No match because the field is empty 1323 | match = false; 1324 | break; 1325 | } 1326 | 1327 | switch(this.filterColumns[col].type) { 1328 | case 'number': 1329 | case 'currency': 1330 | case 'percent': 1331 | case 'date': 1332 | case 'date-local': 1333 | case 'datetime': 1334 | case 'time': 1335 | if (row[fieldName] != this.columnFilterValues[col]) { // Check for exact match on numeric and date fields 1336 | match = false; 1337 | break; 1338 | } 1339 | break; 1340 | default: 1341 | var fieldValue = row[fieldName].toString(); 1342 | var filterValue = this.columnFilterValues[col]; 1343 | if (!this.matchCaseOnFilters) { 1344 | fieldValue = fieldValue.toLowerCase(); 1345 | filterValue = filterValue.toLowerCase(); 1346 | } 1347 | if (fieldValue.search(filterValue) == -1) { // Check for filter value within field value 1348 | match = false; 1349 | break; 1350 | } 1351 | } 1352 | } 1353 | } 1354 | if (match) { 1355 | filteredRows.push(row); 1356 | } 1357 | }); 1358 | this.mydata = filteredRows; 1359 | } 1360 | resolve(); 1361 | }, 0); 1362 | }) 1363 | .then( 1364 | () => this.isWorking = false 1365 | ); 1366 | 1367 | this.filterColumns[this.columnNumber].actions.find(a => a.name == 'clear_'+this.columnNumber).disabled = false; 1368 | this.isFiltered = true; 1369 | } 1370 | 1371 | handleRemove(event) { 1372 | // Pass directly to handleCopy with no additional handling 1373 | } 1374 | 1375 | handleCopyFields(event) { 1376 | // Copy the Pill Contents to the Clipboard 1377 | this.pushClipboard(this.columnFieldParameter); 1378 | } 1379 | 1380 | handleCopyAligns(event) { 1381 | // Copy the Pill Contents to the Clipboard 1382 | this.pushClipboard(this.columnAlignmentParameter); 1383 | } 1384 | 1385 | handleCopyEdits(event) { 1386 | // Copy the Pill Contents to the Clipboard 1387 | this.pushClipboard(this.columnEditParameter); 1388 | } 1389 | 1390 | handleCopyFilters(event) { 1391 | // Copy the Pill Contents to the Clipboard 1392 | this.pushClipboard(this.columnFilterParameter); 1393 | } 1394 | 1395 | handleCopyLabels(event) { 1396 | // Copy the Pill Contents to the Clipboard 1397 | this.pushClipboard(this.columnLabelParameter); 1398 | } 1399 | 1400 | handleCopyWidths(event) { 1401 | // Copy the Pill Contents to the Clipboard 1402 | this.pushClipboard(this.columnWidthParameter); 1403 | } 1404 | 1405 | pushClipboard(content) { 1406 | // Put the selected attribute value in the clipboard 1407 | let inp = this.template.querySelector('.my-clipboard'); 1408 | inp.disabled = false; 1409 | inp.value = content; 1410 | inp.select(); 1411 | document.execCommand('copy'); 1412 | inp.disabled = true; 1413 | } 1414 | 1415 | updateAlignmentParam() { 1416 | // Create the Alignment Label parameter for Config Mode 1417 | var colNum = 0; 1418 | var colString = ''; 1419 | this.filterColumns.forEach(colDef => { 1420 | let configAlign = (this.convertType(colDef['type']) != 'number') ? 'left' : 'right'; 1421 | if (colDef['cellAttributes']['alignment'] != configAlign) { 1422 | colString = colString + ', ' + colDef['fieldName'] + ':' + colDef['cellAttributes']['alignment']; 1423 | } 1424 | colNum += 1; 1425 | }); 1426 | this.columnAlignmentParameter = colString.substring(2); 1427 | } 1428 | 1429 | updateLabelParam() { 1430 | // Create the Column Label parameter for Config Mode 1431 | var colNum = 0; 1432 | var colString = ''; 1433 | this.basicColumns.forEach(colDef => { 1434 | if (colDef['label'] != this.filterColumns[colNum].label) { 1435 | colString = colString + ', ' + colDef['fieldName'] + ':' + this.filterColumns[colNum].label; 1436 | } 1437 | colNum += 1; 1438 | }); 1439 | this.columnLabelParameter = colString.substring(2); 1440 | } 1441 | 1442 | updateEditParam() { 1443 | // Create the Column Edit parameter for Config Mode 1444 | var colNum = 0; 1445 | var colString = ''; 1446 | var allSelected = true; 1447 | this.filterColumns.forEach(colDef => { 1448 | if (colDef['actions'].find(a => a.name == 'aedit_'+colNum).checked) { 1449 | colString = colString + ', ' + colDef['fieldName'] + ':true'; 1450 | } else { 1451 | allSelected = false; 1452 | } 1453 | colNum += 1; 1454 | }); 1455 | this.columnEditParameter = (allSelected) ? 'All' : colString.substring(2); 1456 | } 1457 | 1458 | updateFilterParam() { 1459 | // Create the Column Filter parameter for Config Mode 1460 | var colNum = 0; 1461 | var colString = ''; 1462 | var allSelected = true; 1463 | this.filterColumns.forEach(colDef => { 1464 | if (colDef['actions'].find(a => a.name == 'afilter_'+colNum).checked) { 1465 | colString = colString + ', ' + colDef['fieldName'] + ':true'; 1466 | } else { 1467 | allSelected = false; 1468 | } 1469 | colNum += 1; 1470 | }); 1471 | this.columnFilterParameter = (allSelected) ? 'All' : colString.substring(2); 1472 | } 1473 | 1474 | @api 1475 | validate() { 1476 | // Validation logic to pass back to the Flow 1477 | if(!this.isRequired || this.numberOfRowsSelected > 0) { 1478 | this.setIsInvalidFlag(false); 1479 | return { isValid: true }; 1480 | } 1481 | else { 1482 | // If the component is invalid, return the isValid parameter 1483 | // as false and return an error message. 1484 | this.setIsInvalidFlag(true); 1485 | return { 1486 | isValid: false, 1487 | errorMessage: 'This is a required entry. At least 1 row must be selected.' 1488 | }; 1489 | } 1490 | } 1491 | 1492 | setIsInvalidFlag(value) { 1493 | this.isInvalid = value; 1494 | } 1495 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/datatableV2/datatableV2.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | datatableV2 4 | This component allows the user to configure and display a datatable in a Flow screen. 5 | 49.0 6 | true 7 | 8 | lightning__FlowScreen 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 45 | 48 | 50 | 52 | 54 | 56 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 69 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": ".", 5 | "paths": { 6 | "c/datatableV2": [ 7 | "datatableV2/datatableV2.js" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*", 13 | "../../../../.sfdx/typings/lwc/**/*.d.ts" 14 | ], 15 | "typeAcquisition": { 16 | "include": [ 17 | "jest" 18 | ] 19 | }, 20 | "paths": { 21 | "c/*": [ 22 | "*" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/jsconfig.json~master: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": ".", 5 | "paths": { 6 | "c/datatableV2": [ 7 | "datatableV2/datatableV2.js" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*", 13 | "../../../../.sfdx/typings/lwc/**/*.d.ts" 14 | ], 15 | "typeAcquisition": { 16 | "include": [ 17 | "jest" 18 | ] 19 | }, 20 | "paths": { 21 | "c/*": [ 22 | "*" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/richDatatable/richDatatable.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/richDatatable/richDatatable.js: -------------------------------------------------------------------------------- 1 | import LightningDatatable from "lightning/datatable"; 2 | import richTextColumnType from "./richTextColumnType.html"; 3 | 4 | /** 5 | * Custom component that extends LightningDatatable 6 | * and adds a new column type 7 | */ 8 | export default class richDatatable extends LightningDatatable { 9 | static customTypes={ 10 | // custom type definition 11 | richtext: { 12 | template: richTextColumnType, 13 | standardCellLayout: true 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/richDatatable/richDatatable.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rich-datatable 4 | This component allows a datatable to use richtext. 5 | 49.0 6 | true 7 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/richDatatable/richTextColumnType.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /manifest/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Datatable_Configuration_Helper 5 | Datatable_Configuration_Helper_Temp_SubFlow 6 | Flow 7 | 8 | 9 | SObjectController2 10 | SObjectController2Test 11 | ApexClass 12 | 13 | 14 | datatableV2 15 | richDatatable 16 | LightningComponentBundle 17 | 18 | 49.0 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salesforce-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Salesforce App", 6 | "scripts": { 7 | "lint": "npm run lint:lwc", 8 | "lint:lwc": "eslint force-app/main/default/lwc", 9 | "test": "npm run test:unit", 10 | "test:unit": "sfdx-lwc-jest", 11 | "test:unit:watch": "sfdx-lwc-jest --watch", 12 | "test:unit:debug": "sfdx-lwc-jest --debug", 13 | "test:unit:coverage": "sfdx-lwc-jest --coverage", 14 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 15 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"" 16 | }, 17 | "devDependencies": { 18 | "@prettier/plugin-xml": "^0.7.2", 19 | "@salesforce/eslint-config-lwc": "^0.5.0", 20 | "@salesforce/sfdx-lwc-jest": "^0.7.1", 21 | "eslint": "^6.8.0", 22 | "prettier": "^2.0.5", 23 | "prettier-plugin-apex": "^1.4.0" 24 | } 25 | } -------------------------------------------------------------------------------- /scripts/apex/hello.apex: -------------------------------------------------------------------------------- 1 | // Use .apex files to store anonymous Apex. 2 | // You can execute anonymous Apex in VS Code by selecting the 3 | // apex text and running the command: 4 | // SFDX: Execute Anonymous Apex with Currently Selected Text 5 | // You can also execute the entire file by running the command: 6 | // SFDX: Execute Anonymous Apex with Editor Contents 7 | 8 | string tempvar = 'Enter_your_name_here'; 9 | System.debug('Hello World!'); 10 | System.debug('My name is ' + tempvar); -------------------------------------------------------------------------------- /scripts/soql/account.soql: -------------------------------------------------------------------------------- 1 | // Use .soql files to store SOQL queries. 2 | // You can execute queries in VS Code by selecting the 3 | // query text and running the command: 4 | // SFDX: Execute SOQL Query with Currently Selected Text 5 | 6 | SELECT Id, Name FROM Account; -------------------------------------------------------------------------------- /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": "49.0" 11 | } --------------------------------------------------------------------------------