├── LICENSE ├── README.md └── src ├── applications └── Service_Components.app ├── aura ├── AccountSelector │ ├── AccountSelector.cmp │ ├── AccountSelector.cmp-meta.xml │ ├── AccountSelectorController.js │ └── AccountSelectorHelper.js ├── CaseDatatable │ ├── CaseDatatable.cmp │ ├── CaseDatatable.cmp-meta.xml │ ├── CaseDatatableController.js │ └── CaseDatatableHelper.js ├── ContactAddressForm │ ├── ContactAddressForm.cmp │ ├── ContactAddressForm.cmp-meta.xml │ ├── ContactAddressForm.css │ ├── ContactAddressFormController.js │ └── ContactAddressFormHelper.js ├── ContactDatatable │ ├── ContactDatatable.cmp │ ├── ContactDatatable.cmp-meta.xml │ ├── ContactDatatableController.js │ └── ContactDatatableHelper.js ├── DataService │ ├── DataService.cmp │ ├── DataService.cmp-meta.xml │ ├── DataServiceController.js │ └── DataServiceHelper.js ├── DataTableService │ ├── DataTableService.cmp │ ├── DataTableService.cmp-meta.xml │ ├── DataTableServiceController.js │ └── DataTableServiceHelper.js ├── EventService │ ├── EventService.cmp │ ├── EventService.cmp-meta.xml │ ├── EventServiceController.js │ └── EventServiceHelper.js ├── MessageService │ ├── MessageService.cmp │ ├── MessageService.cmp-meta.xml │ ├── MessageServiceController.js │ └── MessageServiceHelper.js ├── PlatformEventListener │ ├── PlatformEventListener.cmp │ ├── PlatformEventListener.cmp-meta.xml │ ├── PlatformEventListener.css │ ├── PlatformEventListenerController.js │ └── PlatformEventListenerHelper.js ├── QuickUpdateService │ ├── QuickUpdateService.cmp │ ├── QuickUpdateService.cmp-meta.xml │ ├── QuickUpdateServiceController.js │ └── QuickUpdateServiceHelper.js ├── ServiceAppEvent │ ├── ServiceAppEvent.evt │ └── ServiceAppEvent.evt-meta.xml ├── ServiceCompEvent │ ├── ServiceCompEvent.evt │ └── ServiceCompEvent.evt-meta.xml ├── ServiceRecordEvent │ ├── ServiceRecordEvent.evt │ └── ServiceRecordEvent.evt-meta.xml ├── modalFooter │ ├── modalFooter.cmp │ ├── modalFooter.cmp-meta.xml │ └── modalFooterController.js └── onMessage │ ├── onMessage.evt │ └── onMessage.evt-meta.xml ├── classes ├── DataServiceCtrl.cls ├── DataServiceCtrl.cls-meta.xml ├── DataTableService.cls └── DataTableService.cls-meta.xml ├── flexipages ├── Service_Components_Hello_World.flexipage └── Service_Components_UtilityBar.flexipage ├── flows └── Contacts_All.flow ├── objects └── Contact_DML__e.object ├── package.xml ├── profiles └── Admin.profile └── tabs └── SC_Sample_App.tab /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, James H 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Component Design Pattern 2 | 3 | Lighting Web Components addresses many of the issues this design pattern sought to address, so moving forward I will be maintaining only the [lwc-utils repo](https://github.com/tsalb/lwc-utils) where you can find LWC specific design patterns (and utility modules) compared side by side with this deprecated aura service component framework. 4 | 5 | ~~The Service Component design pattern makes it easy for custom components placed separately from each other (not having any parent-child hierarchy) to easily share a single Apex Controller and reduce redundancy of inter-component communication through key-value events.~~ 6 | 7 | ~~Having no component hierarchy makes it more simple to place components anywhere on a lightning page and allowing more flexibility of creating a dynamic user experience out of mixing native and custom components.~~ 8 | 9 | ~~Additionally, the Service Component design pattern allows wrapping of base lightning components to provide much more utility than what is currently offered.~~ 10 | 11 | ~~**tl;dr: Deploy > App Launcher > Service Components**~~ 12 | 13 | 14 | Deploy to Salesforce 16 | 17 | 18 | --- 19 | 20 | The service components in this sample app are: 21 | 22 | `DataService` which encapsulates serverside callouts. A single Apex Controller is attributed to this headless component which uses `aura:methods` to pass parameters to the JS controller which handles serverside configuration like `action.setStorable()` or `action.setParams()`. The action will be passed to `helper.dispatch()` to make the asynchronous callout. 23 | 24 | `EventService` which encapsulates a key-value pair (optional value) model for both application and component events. This component registers and fires generic events which need to be parsed by the handling component(s) via key-value. There is a special recordEvent which is for Lightning Console (since app events broadcast to all console tabs). This can also listen to Platform Events easily and react to them (via `lightning:empApi`). 25 | 26 | `MessageService` which wraps `lightning:overlayLibrary` and provides dynamic creation `aura:methods` for modal bodies and footers. 27 | 28 | `QuickUpdateService` which wraps Lightning Data Service (i.e. `force:recordData`) and provides an `aura:method` to very quickly configure a single-object, single-record DML to any sObject. Since this uses LDS, profile security is respected. This is a POC component. 29 | 30 | `DataTableService` which can quickly generate `tableData` and `tableColumns` in a format expected by `lightning:datatable`. It's designed primarily for read-only, single hierarchy tables. It's still possible to perform further processing either serverside or clientside to configure `lightning:datatable` more granularly. Parent relationship (1 level tested) works and there are helper functions to flatten the data both serverside (column definition) and clientside (data definition). 31 | 32 | --- 33 | 34 | ## DataService Usage Example 35 | Drop this into a component that needs serverside data: 36 | 37 | **AccountSelector.cmp** 38 | ```xml 39 | 40 | 41 | 42 | 43 | ``` 44 | 45 | **AccountSelectorController.js** 46 | ```javascript 47 | doInit: function (component, event, helper) { 48 | helper.service(component).fetchAccountCombobox( 49 | $A.getCallback((error, data) => { 50 | if (data) { 51 | console.log("data from my apex controller is: "+data); 52 | } 53 | }) 54 | ); 55 | }, 56 | ``` 57 | **AccountSelectorHelper.js** 58 | ```javascript 59 | // ServiceHeaderHelper.js 60 | service : function(component) { 61 | return component.find("service"); 62 | }, 63 | ``` 64 | 65 | ## EventService Usage Examples 66 | Some samples from the app: 67 | ```javascript 68 | 69 | helper.eventService(component).fireAppEvent("ACCOUNT_ID_SELECTED", selectedOptionValue); 70 | 71 | helper.eventService(component).fireAppEvent("HEADER_CLEARTABLE"); 72 | 73 | ``` 74 | 75 | ## Handling App, Record, or Comp events with EventService 76 | In any component that needs to listen to these, attach a handler like this: 77 | 78 | **ContactDatatable.cmp** 79 | ```xml 80 | 81 | ``` 82 | **ContactDatatableController.js** 83 | ```javascript 84 | handleApplicationEvent : function(component, event, helper) { 85 | let params = event.getParams(); 86 | switch(params.appEventKey) { 87 | case "ACCOUNT_ID_SELECTED": // fallthrough 88 | case "CONTACTS_UPDATED": 89 | helper.loadContactTable(component, params.appEventValue); 90 | break; 91 | case "HEADER_CLEARTABLE": 92 | component.set("v.tableData", null); 93 | break; 94 | } 95 | }, 96 | ``` 97 | 98 | ## Handling Platform Events with EventService 99 | Using v44, we can leverage `lightning:empApi` to do this: 100 | 101 | **PlatformEventListener.cmp** 102 | ```xml 103 | 106 | ``` 107 | **PlatformEventListenerController.js** 108 | ```javascript 109 | handleContactDmlEvent : function(component, event, helper) { 110 | let payloadJSON = JSON.stringify(event.getParam("payload")); 111 | component.set("v.payloadJSON", payloadJSON); 112 | } 113 | ``` 114 | 115 | ## MessageService Usage Examples 116 | At its core, this is a wrapper around the lightning:overlayLibrary which provides some helper functionality for creating both the body and the footer. There are some special features: 117 | 118 | - Able to handle text or custom component as the modal body. 119 | - Always handles the footer cancel button. 120 | - Specify a main action function which can be either: 121 | - On the originating component by using `component.getReference("someFunction")`. 122 | - On the modal component (the body) that's being created by using `"c.someFunctionOnTheModalComponent"`. 123 | - Pass an Object of parameters to the modal component (the body) from the originating component by using object notation while setting up the modal. 124 | 125 | When you drop in `MessageService.cmp` into a component, such as `ContactDatatable.cmp`, this is an example of how you can open a modal from a function in `ContactDatatableController.js`. 126 | 127 | **ContactDatatableController.js** 128 | ```javascript 129 | handleOpenComponentModal : function(component, event, helper) { 130 | let selectedArr = component.find("searchTable").getSelectedRows(); 131 | 132 | helper.messageService(component).modal( 133 | "update-address-modal", // auraId 134 | "Update Address: "+selectedArr.length+" Row(s)", // headerLabel 135 | "c:ContactAddressForm", // body, MessageService will dynamically create this 136 | { 137 | contactList: selectedArr // bodyParams, MessageService dynamically passes these to c:ContactAddressForm 138 | }, 139 | "c.handleUpdateMultiAddress", // mainActionReference, see above on where you can feed this 140 | "Update" // mainActionLabel 141 | ); 142 | }, 143 | ``` 144 | 145 | The above `c.handleUpdateMultiAddress` is a reference to a function found on `ContactAddressForm.cmp`. `MessageService.cmp` is able to grab reference appropriately and wire it up to the `Update` main action found in the modal footer. 146 | 147 | So, even though overlayLibrary `modalBody` and `modalFooter` are siblings, the footer is referencing a controller action on the body. This makes it easier to write all your container logic on a `modalBody` and leverage `MessageService.cmp` to just open a self-contained `modalBody` component. 148 | 149 | 150 | ## QuickUpdateService Usage Examples 151 | At its core, this is a wrapper around force:recordData which allows for simple single record DML. 152 | 153 | This example from `ContactDatatable.cmp` uses a single button to update multiple fields on a single record. The only attributes `QuickUpdateService.cmp` expects is a `configObject` containing the `recordId` and `fieldUpdates` properties. 154 | 155 | Currently, there is no type checking or much error handling. 156 | 157 | **ContactDatatableHelper.js** 158 | ```javascript 159 | clearMailingAddressWithLightningDataService : function(component, row) { 160 | let _self = this; 161 | let configObject = { // QuickUpdateService only expects this object with recordId and fieldUpdates properties 162 | recordId: row["Id"], 163 | fieldUpdates: { 164 | "MailingStreet": null, 165 | "MailingCity": null, 166 | "MailingState": null, 167 | "MailingPostalCode": null, 168 | "MailingCountry": null 169 | } 170 | } 171 | _self.quickUpdateService(component).LDS_Update( 172 | configObject, 173 | $A.getCallback((saveResult) => { 174 | switch(saveResult.state.toUpperCase()) { 175 | case "SUCCESS": 176 | _self.messageService(component).showToast({ 177 | message: "Cleared Mailing Address.", 178 | variant: "success" 179 | }); 180 | _self.loadContactTable(component, row["AccountId"]); 181 | break; 182 | case "ERROR": 183 | _self.messageService(component).showToast({ 184 | title: "Error Clearing Mailing Address", 185 | message: JSON.stringify(saveResult.error[0].message), 186 | variant: "error", 187 | mode: "pester" 188 | }); 189 | break; 190 | } 191 | }) 192 | ); 193 | }, 194 | ``` 195 | 196 | 197 | ## DataTableService Usage Examples 198 | This is a library service component. It's designed to make read-only lightning:datatable very quick to spin up. 199 | 200 | This example is from `CaseDatatable.cmp` (which is actually created inside a modal from `ContactDatatable.cmp`). The only attributes `DataTableService.cmp` expects is a `tableRequest` Object containing the `queryString` and `bindVars` properties. 201 | 202 | There is no way to fetch the more granular `tableColumns` specific configurations that are offered from `lightning:datatable` however it's possible to post-process the `tableColumns` data even futher serverside OR clientside. 203 | 204 | There is simple handling of parent relationships fields. 205 | 206 | **CaseDatatableController.js** 207 | ```javascript 208 | doInit : function(component, event, helper) { 209 | let contactRecordId = [].concat(component.get("v.contactRecordId")); // guarantees array for idSet 210 | if (!$A.util.isEmpty(contactRecordId)) { 211 | let tableRequest = { 212 | queryString: "SELECT " 213 | + "Id, CaseNumber, CreatedDate, ClosedDate, Description, Comments, Status, Subject, Type, Owner.Name" 214 | + "FROM Case " 215 | + "WHERE ContactId =: idSet " 216 | + "ORDER BY CaseNumber ASC", 217 | bindVars: { 218 | idSet: contactRecordId, 219 | } 220 | } 221 | helper.tableService(component).fetchData( 222 | tableRequest, 223 | $A.getCallback((error, data) => { 224 | if (!$A.util.isEmpty(data)) { 225 | component.set("v.tableData", data.tableData); 226 | component.set("v.tableColumns", data.tableColumns); 227 | } else { 228 | if (!$A.util.isEmpty(error) && error[0].hasOwnProperty("message")) { 229 | helper.messageService(component).showToast({ 230 | message: error[0].message, 231 | variant: "error" 232 | }); 233 | } 234 | } 235 | }) 236 | ); 237 | } 238 | } 239 | ``` 240 | -------------------------------------------------------------------------------- /src/applications/Service_Components.app: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #0070D2 5 | false 6 | 7 | github.com/tsalb/sfdc-lightning-service-components 8 | Large 9 | false 10 | false 11 | 12 | Standard 13 | SC_Sample_App 14 | Lightning 15 | Service_Components_UtilityBar 16 | 17 | -------------------------------------------------------------------------------- /src/aura/AccountSelector/AccountSelector.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Top Accounts with Contacts 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/aura/AccountSelector/AccountSelector.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | Select from a limited set of Accounts 5 | 6 | -------------------------------------------------------------------------------- /src/aura/AccountSelector/AccountSelectorController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | doInit: function (component, event, helper) { 3 | helper.service(component).fetchAccountCombobox( 4 | $A.getCallback((error, data) => { 5 | // This returns whatever datatype is specified in the controller 6 | if (!$A.util.isEmpty(data)) { 7 | component.set("v.topAccounts", JSON.parse(data).items); 8 | } else { 9 | helper.messageService(component).showToast({ 10 | message: "No Accounts in org!", 11 | variant: "error" 12 | }); 13 | } 14 | }) 15 | ); 16 | }, 17 | handleAccountOptionSelected : function(component, event, helper) { 18 | helper.eventService(component).fireAppEvent("ACCOUNT_ID_SELECTED", event.getParam("value")); 19 | }, 20 | handleClearTableOnly : function(component, event, helper) { 21 | helper.eventService(component).fireAppEvent("HEADER_CLEARTABLE"); 22 | }, 23 | }) -------------------------------------------------------------------------------- /src/aura/AccountSelector/AccountSelectorHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | service : function(component) { 3 | return component.find("service"); 4 | }, 5 | messageService : function(component) { 6 | return component.find("messageService"); 7 | }, 8 | eventService : function(component) { 9 | return component.find("eventService"); 10 | }, 11 | }) -------------------------------------------------------------------------------- /src/aura/CaseDatatable/CaseDatatable.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 28 |
29 |
30 |
31 | 32 |
-------------------------------------------------------------------------------- /src/aura/CaseDatatable/CaseDatatable.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | lightning:datatable representation of a Case standard object 5 | 6 | -------------------------------------------------------------------------------- /src/aura/CaseDatatable/CaseDatatableController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | doInit : function(component, event, helper) { 3 | let contactRecordId = [].concat(component.get("v.contactRecordId")); // guarantees array for idSet 4 | if (!$A.util.isEmpty(contactRecordId)) { 5 | let tableRequest = { 6 | queryString: "SELECT " 7 | + "Id, CaseNumber, CreatedDate, ClosedDate, Description, Comments, Status, Subject, Type, Owner.Name " 8 | + "FROM Case " 9 | + "WHERE ContactId =: idSet " 10 | + "ORDER BY CaseNumber ASC", 11 | bindVars: { 12 | idSet: contactRecordId, 13 | } 14 | } 15 | helper.tableService(component).fetchData( 16 | tableRequest, 17 | $A.getCallback((error, data) => { 18 | if (!$A.util.isEmpty(data)) { 19 | component.set("v.tableData", data.tableData); 20 | component.set("v.tableColumns", data.tableColumns); 21 | } else { 22 | if (!$A.util.isEmpty(error) && error[0].hasOwnProperty("message")) { 23 | helper.messageService(component).showToast({ 24 | message: error[0].message, 25 | variant: "error" 26 | }); 27 | } 28 | } 29 | }) 30 | ); 31 | } 32 | } 33 | }) -------------------------------------------------------------------------------- /src/aura/CaseDatatable/CaseDatatableHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | tableService : function(component) { 3 | return component.find("tableService"); 4 | }, 5 | messageService : function(component) { 6 | return component.find("messageService"); 7 | }, 8 | eventService : function(component) { 9 | return component.find("eventService"); 10 | }, 11 | }) -------------------------------------------------------------------------------- /src/aura/ContactAddressForm/ContactAddressForm.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/aura/ContactAddressForm/ContactAddressForm.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | Contact or ContactList address updating form 5 | 6 | -------------------------------------------------------------------------------- /src/aura/ContactAddressForm/ContactAddressForm.css: -------------------------------------------------------------------------------- 1 | .THIS { 2 | } -------------------------------------------------------------------------------- /src/aura/ContactAddressForm/ContactAddressFormController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | doInit : function(component, event, helper) { 3 | let contactList = component.get("v.contactList"); 4 | if (contactList.length == 1) { 5 | component.set("v.singleContactListId", contactList[0].Id); 6 | } 7 | component.set("v.initComplete", true); 8 | }, 9 | handleUpdateMultiAddress : function(component, event, helper) { 10 | helper.updateMultiAddress(component); 11 | }, 12 | }) -------------------------------------------------------------------------------- /src/aura/ContactAddressForm/ContactAddressFormHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | service : function(component) { 3 | return component.find("service"); 4 | }, 5 | messageService : function(component) { 6 | return component.find("messageService"); 7 | }, 8 | eventService : function(component) { 9 | return component.find("eventService"); 10 | }, 11 | updateMultiAddress : function(component) { 12 | let _self = this; 13 | let contactList = component.get("v.contactList"); 14 | let addressObject = component.find("mailing-address").get("v.value"); // contact mailing address is stored in key:value pairs. 15 | _self.service(component).updateMultiContactAddress( 16 | contactList, 17 | addressObject.MailingStreet, 18 | addressObject.MailingCity, 19 | addressObject.MailingState, 20 | addressObject.MailingPostalCode, 21 | addressObject.MailingCountry, 22 | $A.getCallback((error, data) => { 23 | if ($A.util.getBooleanValue(data)) { 24 | _self.messageService(component).showToast({ 25 | message: "Updated Successfully", 26 | variant: "success" 27 | }); 28 | _self.eventService(component).fireAppEvent("CONTACTS_UPDATED", contactList[0].AccountId); 29 | _self.messageService(component).find("overlayLib").notifyClose(); // must be last, as this destroys this component 30 | } else { 31 | if (!$A.util.isEmpty(error) && error[0].hasOwnProperty("message")) { 32 | _self.messageService(component).showToast({ 33 | message: error[0].message, 34 | variant: "error" 35 | }); 36 | } 37 | } 38 | }) 39 | ); 40 | } 41 | }) -------------------------------------------------------------------------------- /src/aura/ContactDatatable/ContactDatatable.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 |
-------------------------------------------------------------------------------- /src/aura/ContactDatatable/ContactDatatable.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | Displays a list of Contacts and their Mailing Address info 5 | 6 | -------------------------------------------------------------------------------- /src/aura/ContactDatatable/ContactDatatableController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | handleRowAction: function (component, event, helper) { 3 | let action = event.getParam("action"); 4 | let row = event.getParam("row"); 5 | switch (action.name) { 6 | case "clear_address": 7 | helper.clearMailingAddressWithLightningDataService(component, row); 8 | break; 9 | case "view_cases": 10 | helper.openViewCasesModal(component, row); 11 | break; 12 | } 13 | }, 14 | handleOpenUpdateAddressModal : function(component, event, helper) { 15 | let selectedArr = component.find("searchTable").getSelectedRows(); 16 | if ($A.util.isEmpty(selectedArr)) { 17 | helper.messageService(component).showToast({ 18 | message: "Please choose at least one Contact." 19 | }); 20 | } else { 21 | helper.messageService(component).modal( 22 | "update-address-modal", 23 | "Update Address: "+selectedArr.length+" Row(s)", 24 | "c:ContactAddressForm", 25 | { 26 | contactList: selectedArr 27 | }, 28 | "c.handleUpdateMultiAddress", 29 | "Update" 30 | ); 31 | } 32 | }, 33 | handleApplicationEvent : function(component, event, helper) { 34 | let params = event.getParams(); 35 | switch(params.appEventKey) { 36 | case "ACCOUNT_ID_SELECTED": // fallthrough 37 | case "CONTACTS_UPDATED": 38 | helper.loadContactTable(component, params.appEventValue); 39 | break; 40 | case "HEADER_CLEARTABLE": 41 | component.set("v.tableData", null); 42 | break; 43 | } 44 | }, 45 | }) -------------------------------------------------------------------------------- /src/aura/ContactDatatable/ContactDatatableHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | service : function(component) { 3 | return component.find("service"); 4 | }, 5 | messageService : function(component) { 6 | return component.find("messageService"); 7 | }, 8 | eventService : function(component) { 9 | return component.find("eventService"); 10 | }, 11 | quickUpdateService : function(component) { 12 | return component.find("quickUpdateService"); 13 | }, 14 | getTableColumnDefinition : function () { 15 | let tableColumns = [ 16 | { 17 | label: "Name", 18 | fieldName: "Name", 19 | type: "text", 20 | initialWidth: 110 21 | }, 22 | { 23 | label: "Email", 24 | fieldName: "Email", 25 | type: "email", 26 | initialWidth: 170 27 | }, 28 | { 29 | label: "Phone", 30 | fieldName: "Phone", 31 | type: "phone", 32 | initialWidth: 130 33 | }, 34 | { 35 | label: "Street", 36 | fieldName: "MailingStreet", 37 | type: "text", 38 | }, 39 | { 40 | label: "City", 41 | fieldName: "MailingCity", 42 | type: "text", 43 | }, 44 | { 45 | label: "State", 46 | fieldName: "MailingState", 47 | type: "text", 48 | }, 49 | { 50 | label: "Zip", 51 | fieldName: "MailingPostalCode", 52 | type: "text", 53 | }, 54 | { 55 | label: "Country", 56 | fieldName: "MailingCountry", 57 | type: "text", 58 | }, 59 | { 60 | type: 'button', 61 | initialWidth: 135, 62 | typeAttributes: { 63 | label: 'Clear Address', 64 | name: 'clear_address', 65 | title: 'Click to clear out Mailing Address' 66 | } 67 | }, 68 | { 69 | type: 'button', 70 | initialWidth: 130, 71 | typeAttributes: { 72 | label: 'View Cases', 73 | name: 'view_cases', 74 | title: 'Click to view all cases against this Contact' 75 | } 76 | }, 77 | ]; 78 | return tableColumns; 79 | }, 80 | clearMailingAddressWithLightningDataService : function(component, row) { 81 | let _self = this; 82 | let configObject = { // QuickUpdateService only expects this object with recordId and fieldUpdates properties 83 | recordId: row["Id"], 84 | fieldUpdates: { 85 | "MailingStreet": null, 86 | "MailingCity": null, 87 | "MailingState": null, 88 | "MailingPostalCode": null, 89 | "MailingCountry": null 90 | } 91 | } 92 | _self.quickUpdateService(component).LDS_Update( 93 | configObject, 94 | $A.getCallback((saveResult) => { 95 | switch(saveResult.state.toUpperCase()) { 96 | case "SUCCESS": 97 | _self.messageService(component).showToast({ 98 | message: "Cleared Mailing Address.", 99 | variant: "success" 100 | }); 101 | _self.loadContactTable(component, row["AccountId"]); 102 | break; 103 | case "ERROR": 104 | _self.messageService(component).showToast({ 105 | title: "Error Clearing Mailing Address", 106 | message: JSON.stringify(saveResult.error[0].message), 107 | variant: "error", 108 | mode: "pester" 109 | }); 110 | break; 111 | } 112 | }) 113 | ); 114 | }, 115 | openViewCasesModal : function(component, row) { 116 | let _self = this; 117 | _self.messageService(component).bodyModalLarge( 118 | "view-cases-modal", 119 | "Cases For "+row["Name"], 120 | "c:CaseDatatable", 121 | { 122 | contactRecordId: row["Id"] 123 | } 124 | ); 125 | }, 126 | loadContactTable : function(component, accountId) { 127 | let _self = this; 128 | _self.service(component).fetchContactsByAccountId( 129 | accountId, 130 | $A.getCallback((error, data) => { 131 | if (!$A.util.isEmpty(data)) { 132 | component.set("v.tableData", data); 133 | component.set("v.tableColumns", _self.getTableColumnDefinition()); 134 | } else { 135 | if (!$A.util.isEmpty(error) && error[0].hasOwnProperty("message")) { 136 | _self.messageService(component).showToast({ 137 | message: error[0].message, 138 | variant: "error", 139 | mode: "pester" 140 | }); 141 | } 142 | } 143 | }) 144 | ); 145 | }, 146 | }) -------------------------------------------------------------------------------- /src/aura/DataService/DataService.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/aura/DataService/DataService.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 44.0 4 | Contains methods shared across components, backed by Apex Controller 5 | 6 | -------------------------------------------------------------------------------- /src/aura/DataService/DataServiceController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | handleFetchAccountCombobox : function(component, event, helper) { 3 | let params = event.getParam("arguments"); 4 | let action = component.get("c.getAccountOptions"); 5 | helper.dispatchAction(component, action, params); 6 | }, 7 | handleFetchContactsByAccountId : function(component, event, helper) { 8 | let params = event.getParam("arguments"); 9 | let action = component.get("c.getContactsByAccountId"); 10 | action.setParams({ 11 | accountId : params.accountIdEventArg 12 | }); 13 | helper.dispatchAction(component, action, params); 14 | }, 15 | handleUpdateMultiContactAddress : function(component, event, helper) { 16 | let params = event.getParam("arguments"); 17 | let action = component.get("c.updateMultiContactAddress"); 18 | action.setParams({ 19 | conList : params.contactList, 20 | conStreet : params.contactMailingStreet, 21 | conCity : params.contactMailingCity, 22 | conState : params.contactMailingState, 23 | conZip : params.contactMailingZip, 24 | conCountry : params.contactMailingCountry 25 | }); 26 | helper.dispatchAction(component, action, params); 27 | }, 28 | }) -------------------------------------------------------------------------------- /src/aura/DataService/DataServiceHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | dispatchAction : function(component, action, params) { 3 | action.setCallback(this, (response) => { 4 | if (response.getState() === "SUCCESS") { 5 | params.callback(null, response.getReturnValue()); 6 | } else { 7 | params.callback(response.getError()); 8 | } 9 | }); 10 | $A.enqueueAction(action); 11 | } 12 | }) -------------------------------------------------------------------------------- /src/aura/DataTableService/DataTableService.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/aura/DataTableService/DataTableService.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | A helper library to quickly generate lightning:datatable attributes with a simple SOQL 5 | 6 | -------------------------------------------------------------------------------- /src/aura/DataTableService/DataTableServiceController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | handleFetchData : function(component, event, helper) { 3 | let params = event.getParam("arguments"); 4 | let action = component.get("c.createTableCache"); 5 | action.setParams({ 6 | tableRequest : params.tableRequest 7 | }); 8 | helper.dispatchAction(component, action, params); 9 | }, 10 | }) -------------------------------------------------------------------------------- /src/aura/DataTableService/DataTableServiceHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | dispatchAction : function(component, action, params) { 3 | let _self = this; 4 | action.setCallback(this, function(response) { 5 | if (response.getState() === "SUCCESS") { 6 | let resp = response.getReturnValue(); 7 | _self.flattenQueryResult(resp.tableData) 8 | .then($A.getCallback((result) => { 9 | let fullResp = { 10 | tableData: result, 11 | tableColumns: resp.tableColumns 12 | } 13 | params.callback(null, fullResp); 14 | })) 15 | .catch((error) => { 16 | params.callback(error); 17 | }); 18 | } else { 19 | params.callback(response.getError()); 20 | } 21 | }); 22 | $A.enqueueAction(action); 23 | }, 24 | flattenQueryResult : function(listOfObjects) { 25 | let _self = this; 26 | return new Promise($A.getCallback((resolve, reject) => { 27 | if ($A.util.isEmpty(listOfObjects)) { 28 | resolve(new Array()); 29 | } else { 30 | for (let i = 0; i < listOfObjects.length; i++) { 31 | let obj = listOfObjects[i]; 32 | for(let prop in obj) { 33 | if (!obj.hasOwnProperty(prop)) { 34 | continue; 35 | } 36 | if (typeof obj[prop] == 'object' && typeof obj[prop] != 'Array') { 37 | obj = Object.assign(obj, _self.flattenObject(prop,obj[prop])); 38 | } else if (typeof obj[prop] == 'Array') { 39 | for(let j = 0; j < obj[prop].length; j++) { 40 | obj[prop+'_'+j] = Object.assign(obj, _self.flattenObject(prop,obj[prop])); 41 | } 42 | } 43 | } 44 | } 45 | resolve(listOfObjects); 46 | } 47 | })); 48 | }, 49 | flattenObject : function(propName, obj) { 50 | let _self = this; 51 | let flatObject = []; 52 | for (let prop in obj) { 53 | //if this property is an object, we need to flatten again 54 | let propIsNumber = isNaN(propName); 55 | let preAppend = propIsNumber ? propName+'_' : ''; 56 | 57 | if (typeof obj[prop] == 'object') { 58 | flatObject[preAppend+prop] = Object.assign(flatObject, _self.flattenObject(preAppend+prop,obj[prop])); 59 | } else { 60 | flatObject[preAppend+prop] = obj[prop]; 61 | } 62 | } 63 | return flatObject; 64 | }, 65 | }) -------------------------------------------------------------------------------- /src/aura/EventService/EventService.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/aura/EventService/EventService.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 44.0 4 | Contains events shared across components, drop this into a component to register it as being able to fire an event 5 | 6 | -------------------------------------------------------------------------------- /src/aura/EventService/EventServiceController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | doInit : function(component, event, helper) { 3 | if (!$A.util.isEmpty(component.get("v.channel"))) { 4 | helper.subscribe(component, event); 5 | } 6 | }, 7 | handleDestroy : function(component, event, helper) { 8 | if (!$A.util.isEmpty(component.get("v.channel")) && component.isValid()) { 9 | helper.unsubscribe(component, event); 10 | } 11 | }, 12 | handleFireApplicationEvent : function(component, event) { 13 | let params = event.getParam("arguments"); 14 | let appEvent = $A.get("e.c:ServiceAppEvent"); 15 | 16 | appEvent.setParams({ 17 | appEventKey : params.eventKey, 18 | appEventValue : params.eventValue 19 | }); 20 | appEvent.fire(); 21 | }, 22 | handleFireRecordEvent : function(component, event, helper) { 23 | let recordEventScope = component.get("v.recordEventScope"); 24 | 25 | if ($A.util.isUndefinedOrNull(recordEventScope)) { 26 | alert("recordEventScope missing, cannot fire record event!"); 27 | } else { 28 | let params = event.getParam("arguments"); 29 | let recordEvent = $A.get("e.c:ServiceRecordEvent"); 30 | 31 | recordEvent.setParams({ 32 | recordEventKey : params.eventKey, 33 | recordEventValue : params.eventValue, 34 | recordEventScope : recordEventScope 35 | }); 36 | recordEvent.fire(); 37 | } 38 | }, 39 | handleFireComponentEvent : function(component, event) { 40 | let params = event.getParam("arguments"); 41 | let compEvent = component.getEvent("ServiceCompEvent"); 42 | 43 | compEvent.setParams({ 44 | compEventKey : params.eventKey, 45 | compEventValue : params.eventValue 46 | }); 47 | compEvent.fire(); 48 | }, 49 | }) -------------------------------------------------------------------------------- /src/aura/EventService/EventServiceHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | subscribe : function(component, event) { 3 | let empApi = component.find("empApi"); 4 | let channel = component.get("v.channel"); 5 | let replayId = component.get("v.replayIdStream"); // Specify -1 to get new events from the tip of the stream, -2 to replay from the last saved event 6 | let isDebug = component.get("v.setDebugFlag"); 7 | 8 | // Callback function to be passed in the subscribe call. 9 | // After an event is received, this callback prints the event 10 | // payload to the console. 11 | let callback = function (message) { 12 | if (isDebug) { 13 | console.log("Received [" + message.channel + " : " + message.data.event.replayId + "] payload=" + JSON.stringify(message.data.payload)); 14 | } 15 | component.getEvent("onMessage") 16 | .setParams({ payload: message.data.payload }) 17 | .fire(); 18 | }.bind(this); 19 | 20 | // Error handler function that prints the error to the console. 21 | let errorHandler = function (message) { 22 | if (isDebug) { 23 | console.log("Received error ", message); 24 | } 25 | }.bind(this); 26 | 27 | // Register error listener and pass in the error handler function. 28 | empApi.onError(errorHandler); 29 | 30 | empApi.subscribe(channel, replayId, callback).then(function(sub) { 31 | component.set("v.sub", sub); 32 | }); 33 | }, 34 | unsubscribe : function(component, event) { 35 | let empApi = component.find("empApi"); 36 | let channel = component.get("v.channel"); 37 | let isDebug = component.get("v.setDebugFlag"); 38 | 39 | // Callback function to be passed in the subscribe call. 40 | let callback = function (message) { 41 | if (isDebug) { 42 | console.log("Unsubscribed from channel " + channel); 43 | } 44 | }.bind(this); 45 | 46 | // Error handler function that prints the error to the console. 47 | let errorHandler = function (message) { 48 | if (isDebug) { 49 | console.log("Received error ", message); 50 | } 51 | }.bind(this); 52 | 53 | // Object that contains subscription attributes used to 54 | // unsubscribe. 55 | let sub = { 56 | "id": component.get("v.sub")["id"], 57 | "channel": component.get("v.sub")["channel"] 58 | }; 59 | 60 | // Register error listener and pass in the error handler function. 61 | empApi.onError(errorHandler); 62 | 63 | // Unsubscribe from the channel using the sub object. 64 | empApi.unsubscribe(sub, callback); 65 | } 66 | }) -------------------------------------------------------------------------------- /src/aura/MessageService/MessageService.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/aura/MessageService/MessageService.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 44.0 4 | For creating notifications and modals in the LDS 5 | 6 | -------------------------------------------------------------------------------- /src/aura/MessageService/MessageServiceController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | handleShowToast : function(component, event, helper) { 3 | // pass the config object through 4 | helper.notificationsLib(component).showToast(event.getParam("arguments")["configObj"]); 5 | }, 6 | createOverlayModal : function(component, event, helper) { 7 | let params = event.getParam("arguments"); 8 | // Creating the body first - this can be a custom component or text wrapped in formattedText 9 | helper.createBody(params, 10 | $A.getCallback((error, modalBody) => { 11 | if (error) { 12 | alert(error); 13 | return; 14 | } 15 | if (modalBody.isValid() && !$A.util.isEmpty(modalBody)) { 16 | // if mainActionReference has a c. prefix, it means we want an action on the body just created 17 | let str = String(params.mainActionReference); 18 | if (str.startsWith("c.")) { 19 | params.mainActionReference = modalBody.getReference(params.mainActionReference); 20 | } 21 | helper.createButton(params, 22 | $A.getCallback((error, mainAction) => { 23 | if (error) { 24 | alert(error); 25 | return; 26 | } 27 | if (mainAction.isValid() && !$A.util.isEmpty(mainAction)) { 28 | // Final assembly 29 | $A.createComponent( 30 | "c:modalFooter", 31 | { 32 | "actions": mainAction 33 | }, 34 | (completedFooter, status, errorMessage) => { 35 | if (status === "SUCCESS") { 36 | helper.overlayLib(component).showCustomModal({ 37 | header: params.headerLabel, 38 | body: modalBody, 39 | footer: completedFooter, 40 | showCloseButton: helper.defineShowCLoseButtonAttribute(params.showCloseButton), 41 | cssClass: helper.defineLargeModalAttribute(params.isLargeModal) 42 | }) 43 | .then($A.getCallback((overlay) => { 44 | if (!$A.util.isEmpty(params.bodyParams)) { 45 | Object.keys(params.bodyParams) 46 | .forEach((v,i,a) => { 47 | let valueProviderAdded = "v."+v; 48 | modalBody.set(valueProviderAdded, params.bodyParams[v]); 49 | }); 50 | } 51 | helper.eventService(component).fireAppEvent("MODAL_READY"); 52 | if (!$A.util.isEmpty(params.callback)) { 53 | params.callback(overlay); 54 | } 55 | })); 56 | } 57 | } 58 | ); 59 | } else { 60 | console.log("mainAction error is: "+error[0].message); 61 | } 62 | }) 63 | ); // end helper.createButton 64 | } else { 65 | console.log("modalBody error is: "+error[0].message); 66 | } 67 | }) 68 | ); // end helper.createBody 69 | }, 70 | createOverlayModalWithoutFooter : function(component, event, helper) { 71 | let params = event.getParam("arguments"); 72 | helper.createBody(params, 73 | $A.getCallback((error, modalBody) => { 74 | if (error) { 75 | alert(error); 76 | return; 77 | } 78 | if (modalBody.isValid() && !$A.util.isEmpty(modalBody)) { 79 | helper.overlayLib(component).showCustomModal({ 80 | header: params.headerLabel, 81 | body: modalBody, 82 | showCloseButton: true, 83 | cssClass: helper.defineLargeModalAttribute(params.isLargeModal) 84 | }) 85 | .then($A.getCallback((overlay) => { 86 | if (!$A.util.isEmpty(params.bodyParams)) { 87 | Object.keys(params.bodyParams).forEach((v,i,a) => { 88 | let valueProviderAdded = "v."+v; 89 | modalBody.set(valueProviderAdded, params.bodyParams[v]); 90 | }); 91 | } 92 | helper.eventService(component).fireAppEvent("MODAL_READY"); 93 | if (!$A.util.isEmpty(params.callback)) { 94 | params.callback(overlay); 95 | } 96 | })); 97 | } else { 98 | console.log("modalBody error is: "+error[0].message); 99 | } 100 | }) 101 | ); // end helper.createBody 102 | }, 103 | }) -------------------------------------------------------------------------------- /src/aura/MessageService/MessageServiceHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | eventService : function(component) { 3 | return component.find("eventService"); 4 | }, 5 | overlayLib : function(component) { 6 | return component.find("overlayLib"); 7 | }, 8 | notificationsLib : function(component) { 9 | return component.find("notificationsLib"); 10 | }, 11 | createBody : function(params, ctrlCallback) { 12 | let componentType = params.body.split(":")[0]; 13 | let componentParams = {}; 14 | // if we had some bodyParams, let's set the target modal body with their data 15 | if (!$A.util.isEmpty(params.bodyParams)) { 16 | Object.keys(params.bodyParams) 17 | .forEach((v,i,a) => { 18 | componentParams[v] = params.bodyParams[v]; 19 | }); 20 | } 21 | switch(componentType) { 22 | case "c" : //custom component 23 | $A.createComponent( 24 | params.body, 25 | componentParams, 26 | (newModalBody, status, errorMessage) => { 27 | if (status === "SUCCESS") { 28 | ctrlCallback(null, newModalBody); 29 | } else { 30 | ctrlCallback(errorMessage); 31 | } 32 | } 33 | ); 34 | break; 35 | default: 36 | $A.createComponent( 37 | "lightning:formattedText", 38 | { 39 | "value": params.body, 40 | "class": "slds-align_absolute-center" 41 | }, 42 | (formattedText, status, errorMessage) => { 43 | if (status === "SUCCESS") { 44 | ctrlCallback(null, formattedText); 45 | } else { 46 | ctrlCallback(errorMessage); 47 | } 48 | } 49 | ); 50 | } 51 | }, 52 | createButton : function(params, ctrlCallback) { 53 | $A.createComponent( 54 | "lightning:button", 55 | { 56 | "aura:id": params.auraId+"-main-action", 57 | "label": params.mainActionLabel, 58 | "onclick": params.mainActionReference, 59 | "variant": "brand" 60 | }, 61 | (newButton, status, errorMessage) => { 62 | if (status === "SUCCESS") { 63 | ctrlCallback(null, newButton); 64 | } else { 65 | ctrlCallback(errorMessage); 66 | } 67 | } 68 | ); 69 | }, 70 | defineLargeModalAttribute : function(isLargeModalVal) { 71 | if ($A.util.isUndefinedOrNull(isLargeModalVal)) { 72 | return null; 73 | } 74 | if (!$A.util.getBooleanValue(isLargeModalVal)) { 75 | return null; 76 | } 77 | if ($A.util.getBooleanValue(isLargeModalVal)) { 78 | return "slds-modal_large"; 79 | } 80 | }, 81 | defineShowCLoseButtonAttribute : function(showCloseButtonBooleanVal) { 82 | if ($A.util.isUndefinedOrNull(showCloseButtonBooleanVal)) { 83 | return true; 84 | } 85 | if (!$A.util.getBooleanValue(showCloseButtonBooleanVal)) { 86 | return false; 87 | } 88 | if ($A.util.getBooleanValue(showCloseButtonBooleanVal)) { 89 | return true; 90 | } 91 | } 92 | }) -------------------------------------------------------------------------------- /src/aura/PlatformEventListener/PlatformEventListener.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | {! v.payloadJSON } 14 |
15 |
16 |
17 |
18 | 19 |
-------------------------------------------------------------------------------- /src/aura/PlatformEventListener/PlatformEventListener.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 44.0 4 | To showcase lightning:empApi 5 | 6 | -------------------------------------------------------------------------------- /src/aura/PlatformEventListener/PlatformEventListener.css: -------------------------------------------------------------------------------- 1 | .THIS { 2 | } -------------------------------------------------------------------------------- /src/aura/PlatformEventListener/PlatformEventListenerController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | handleContactDmlEvent : function(component, event, helper) { 3 | let payloadJSON = JSON.stringify(event.getParam("payload")); 4 | component.set("v.payloadJSON", payloadJSON); 5 | } 6 | }) -------------------------------------------------------------------------------- /src/aura/PlatformEventListener/PlatformEventListenerHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | messageService : function(component) { 3 | return component.find("messageService"); 4 | }, 5 | eventService : function(component) { 6 | return component.find("eventService"); 7 | }, 8 | }) -------------------------------------------------------------------------------- /src/aura/QuickUpdateService/QuickUpdateService.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/aura/QuickUpdateService/QuickUpdateService.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | Leverages Lightning Data Service to perform quick single-object, single-record DMLs. 5 | 6 | -------------------------------------------------------------------------------- /src/aura/QuickUpdateService/QuickUpdateServiceController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | initUpdatePromiseChain : function(component, event, helper) { 3 | helper.initializeLightningDataService(component, event) 4 | .then($A.getCallback((callback) => { 5 | return helper.pollCheckWhenFullyLoaded(component, event, callback); 6 | })) 7 | .catch((error) => { 8 | $A.reportError("Promise Error", error); 9 | helper.messageService(component).showToast({ 10 | message: error, 11 | variant: "error", 12 | mode: "pester" 13 | }); 14 | }); 15 | }, 16 | handleRecordUpdated : function(component, event, helper) { 17 | let changeType = event.getParams().changeType; 18 | switch(changeType.toUpperCase()) { 19 | case "ERROR": 20 | helper.messageService(component).showToast({ 21 | title: "Error in LDS", 22 | message: component.get("v.simpleRecordError"), 23 | variant: "error", 24 | mode: "pester" 25 | }); 26 | break; 27 | case "LOADED" : 28 | component.set("v.lightningDataServiceLoaded", true); 29 | break; 30 | case "CHANGED": 31 | // destroy using aura:if 32 | component.set("v.transactionInProgress", false); 33 | // clear everything 34 | component.set("v.fieldUpdates", null); 35 | component.set("v.fieldApiNameToUpdateValueMap", null); 36 | component.set("v.recordId", null); 37 | component.set("v.fields", null); 38 | component.set("v.simpleRecord", null); 39 | component.set("v.simpleRecordError", null); 40 | component.set("v.lightningDataServiceLoaded", false); 41 | break; 42 | } 43 | }, 44 | }) -------------------------------------------------------------------------------- /src/aura/QuickUpdateService/QuickUpdateServiceHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | messageService : function(component) { 3 | return component.find("messageService"); 4 | }, 5 | initializeLightningDataService : function(component, event) { 6 | return new Promise($A.getCallback((resolve, reject) => { 7 | let params = event.getParam("arguments"); 8 | let fieldApiNameToUpdateValueMap = new Map(); 9 | let recordId = params.configObject["recordId"]; 10 | let fieldUpdates = params.configObject["fieldUpdates"]; 11 | 12 | if (!$A.util.isEmpty(fieldUpdates)) { 13 | Object.keys(fieldUpdates).forEach(function(v,i,a) { 14 | fieldApiNameToUpdateValueMap.set(v, fieldUpdates[v]); 15 | }); 16 | } 17 | 18 | component.set("v.recordId", recordId); 19 | component.set("v.fieldApiNameToUpdateValueMap", fieldApiNameToUpdateValueMap); 20 | component.set("v.fields", Array.from(fieldApiNameToUpdateValueMap.keys())); 21 | 22 | // init using aura:if 23 | component.set("v.transactionInProgress", true); 24 | 25 | // pass the callback down the chain 26 | resolve(params.callback); 27 | })); 28 | }, 29 | pollCheckWhenFullyLoaded : function(component, event, callback) { 30 | let _self = this; 31 | if ($A.util.getBooleanValue(component.get("v.lightningDataServiceLoaded"))) { 32 | _self.updateWithLightningDataService(component, event, callback); 33 | } else { 34 | window.setTimeout( 35 | $A.getCallback(() => { 36 | _self.pollCheckWhenFullyLoaded(component, event, callback) 37 | }), 500 38 | ); 39 | } 40 | }, 41 | updateWithLightningDataService : function(component, event, callback) { 42 | let _self = this; 43 | let fieldApiNameToUpdateValueMap = component.get("v.fieldApiNameToUpdateValueMap"); 44 | let simpleRecord = component.get("v.simpleRecord"); 45 | 46 | for (let apiName of fieldApiNameToUpdateValueMap.keys()) { 47 | let updateValue = fieldApiNameToUpdateValueMap.get(apiName); 48 | simpleRecord[apiName] = $A.util.isEmpty(updateValue) || updateValue === "null" 49 | ? null 50 | : updateValue; 51 | } 52 | component.find("lds").saveRecord( 53 | $A.getCallback((saveResult) => { 54 | callback(saveResult); 55 | }) 56 | ); 57 | }, 58 | }) -------------------------------------------------------------------------------- /src/aura/ServiceAppEvent/ServiceAppEvent.evt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/aura/ServiceAppEvent/ServiceAppEvent.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Key and value pair application event 5 | 6 | -------------------------------------------------------------------------------- /src/aura/ServiceCompEvent/ServiceCompEvent.evt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/aura/ServiceCompEvent/ServiceCompEvent.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Key and value pair component event 5 | 6 | -------------------------------------------------------------------------------- /src/aura/ServiceRecordEvent/ServiceRecordEvent.evt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/aura/ServiceRecordEvent/ServiceRecordEvent.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 44.0 4 | Key and value pair application event, but with a scoped parameter (like recordId) 5 | 6 | -------------------------------------------------------------------------------- /src/aura/modalFooter/modalFooter.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {! v.actions } 7 | -------------------------------------------------------------------------------- /src/aura/modalFooter/modalFooter.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | Utility component for dynamic modals (overlays) 5 | 6 | -------------------------------------------------------------------------------- /src/aura/modalFooter/modalFooterController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | handleCancel : function(component, event, helper) { 3 | component.find("overlayLib").notifyClose(); 4 | } 5 | }) -------------------------------------------------------------------------------- /src/aura/onMessage/onMessage.evt: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/aura/onMessage/onMessage.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | Emits platform event to implementing components 5 | 6 | -------------------------------------------------------------------------------- /src/classes/DataServiceCtrl.cls: -------------------------------------------------------------------------------- 1 | /************************************************************* 2 | * @author: James Hou, james@sparkworks.io 3 | * Date: 8-2018 4 | **************************************************************/ 5 | 6 | public with sharing class DataServiceCtrl { 7 | 8 | @AuraEnabled 9 | public static String getAccountOptions() { 10 | Data valueLabels = new Data(); 11 | List aggList = new List([ 12 | SELECT 13 | AccountId accId, 14 | Account.Name accName 15 | FROM 16 | Contact 17 | GROUP BY 18 | AccountId, Account.Name 19 | HAVING 20 | count(Name) > 0 21 | ORDER BY 22 | count(Name) DESC 23 | LIMIT 10 24 | ]); 25 | for (AggregateResult ar : aggList) { 26 | valueLabels.items.add(new Item((String)ar.get('accId'), (String)ar.get('accName'))); 27 | } 28 | //we need to serialize this because lightning cant have @AuraEnabled on inner classes 29 | return JSON.serialize(valueLabels); 30 | } 31 | 32 | @AuraEnabled 33 | public static List getContactsByAccountId(String accountId) { 34 | try { 35 | return [ 36 | SELECT 37 | Name, 38 | Email, 39 | Phone, 40 | MailingStreet, 41 | MailingCity, 42 | MailingState, 43 | MailingPostalCode, 44 | MailingCountry, 45 | AccountId, 46 | Id 47 | FROM 48 | Contact 49 | WHERE 50 | AccountId =: accountId 51 | ]; 52 | } catch (Exception e) { 53 | throw new AuraHandledException(e.getMessage()); 54 | } 55 | } 56 | 57 | @AuraEnabled 58 | public static Boolean updateMultiContactAddress(List conList, String conStreet, String conCity, String conState, String conZip, String conCountry) { 59 | List contactsToUpdate = new List(); 60 | for (Contact c : conList) { 61 | contactsToUpdate.add( 62 | new Contact( 63 | Id = c.Id, 64 | MailingStreet = conStreet, 65 | MailingCity = conCity, 66 | MailingState = conState, 67 | MailingPostalCode = conZip, 68 | MailingCountry = conCountry 69 | ) 70 | ); 71 | } 72 | try { 73 | update contactsToUpdate; 74 | return true; 75 | } catch (Exception e) { 76 | throw new AuraHandledException(e.getMessage()); 77 | return false; 78 | } 79 | } 80 | 81 | public class Item { 82 | public String value; 83 | public String label; 84 | 85 | public Item(String value, String label) { 86 | this.value = value; 87 | this.label = label; 88 | } 89 | } 90 | 91 | public class Data { 92 | public List items; 93 | 94 | public Data() { 95 | this.items = new List(); 96 | } 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/classes/DataServiceCtrl.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/DataTableService.cls: -------------------------------------------------------------------------------- 1 | /************************************************************* 2 | * @author: James Hou, james@sparkworks.io 3 | **************************************************************/ 4 | 5 | public with sharing class DataTableService { 6 | 7 | // tableRequest 8 | public static final String QUERY_STRING_KEY = 'queryString'; 9 | public static final String BIND_VAR_KEY = 'bindVars'; 10 | public static final String ID_SET_KEY = 'idSet'; 11 | 12 | // tableCache 13 | public static final String TABLE_DATA_KEY = 'tableData'; 14 | public static final String TABLE_COLUMNS_KEY = 'tableColumns'; 15 | 16 | // lightning:datatable type translation map 17 | // https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_enum_Schema_DisplayType.htm 18 | // https://developer.salesforce.com/docs/component-library/bundle/lightning:datatable/documentation 19 | public static final Map DISPLAY_TYPE_TO_DATATABLE_TYPE_MAP = new Map{ 20 | Schema.DisplayType.address => 'text', 21 | Schema.DisplayType.anytype => 'text', 22 | Schema.DisplayType.base64 => 'text', 23 | Schema.DisplayType.Boolean => 'boolean', 24 | Schema.DisplayType.Combobox => 'text', 25 | Schema.DisplayType.Currency => 'currency', 26 | Schema.DisplayType.Date => 'date-local', // my preference 27 | Schema.DisplayType.DateTime => 'date-local', // my preference 28 | Schema.DisplayType.Double => 'number', 29 | Schema.DisplayType.Email => 'email', 30 | Schema.DisplayType.ID => 'text', 31 | Schema.DisplayType.Integer => 'number', 32 | Schema.DisplayType.MultiPicklist => 'text', 33 | Schema.DisplayType.Percent => 'percent', 34 | Schema.DisplayType.Phone => 'text', 35 | Schema.DisplayType.Picklist => 'text', 36 | Schema.DisplayType.Reference => 'text', 37 | Schema.DisplayType.String => 'text', 38 | Schema.DisplayType.TextArea => 'text', 39 | Schema.DisplayType.Time => 'text', 40 | Schema.DisplayType.URL => 'url' 41 | }; 42 | 43 | /** 44 | * Creates a lightning:datatable ready object keys: 45 | * tableData and tableColumns can be used as attributes directly clientside. 46 | * 47 | * @param tableRequest [Object with configs, see DataTableService.cmp] 48 | * @return [Object with tableCache.tableData, tableCache.tableColumns] 49 | */ 50 | @AuraEnabled 51 | public static Map createTableCache(Map tableRequest) { 52 | if (!tableRequest.containsKey(QUERY_STRING_KEY)) { 53 | throw new AuraHandledException('tableService queryString is missing.'); 54 | } 55 | Map tableServiceResponse = new Map(); 56 | List tableData = DataTableService.getSObjectData(tableRequest); 57 | String queryString = (String)tableRequest.get(QUERY_STRING_KEY); 58 | String sObjectName = queryString.substringAfterLast(' FROM ').split(' ').get(0); // don't depend on if there is a WHERE, also in case FROM is in a field name 59 | SObject queryObject = Schema.getGlobalDescribe().get(sObjectName).newSObject(); 60 | 61 | tableServiceResponse.put(TABLE_DATA_KEY, tableData); 62 | tableServiceResponse.put(TABLE_COLUMNS_KEY, DataTableService.getColumnData(queryString, queryObject)); 63 | return tableServiceResponse; 64 | } 65 | 66 | /** 67 | * Routing method to see if there are any Binding Variables (BIND_VAR_KEY) to scope the dynamic query 68 | * 69 | * @param tableRequest [Object with configs] 70 | */ 71 | private static List getSObjectData(Map tableRequest) { 72 | if (tableRequest.get(BIND_VAR_KEY) == null) { 73 | return DataTableService.getSObjectDataFromQueryString((String)tableRequest.get(QUERY_STRING_KEY)); 74 | } else { 75 | return DataTableService.getSObjectDataFromQueryString((String)tableRequest.get(QUERY_STRING_KEY), tableRequest.get(BIND_VAR_KEY)); 76 | } 77 | } 78 | 79 | /** 80 | * No dynamic binding vars, returns everything specific directly from SOQL string 81 | * 82 | * @param queryString [Dynamic SOQL string] 83 | * @return [List of dynamically queried SObjects] 84 | */ 85 | private static List getSObjectDataFromQueryString(String queryString) { 86 | try { 87 | System.debug('getSObjectDataFromQueryString queryString is: '+queryString); 88 | return Database.query(queryString); 89 | } catch (Exception e) { 90 | throw new AuraHandledException(e.getMessage()); 91 | } 92 | } 93 | 94 | /** 95 | * Contains dynamic binding vars, returns everything bound to the dynamic variable 96 | * 97 | * @param queryString [Dynamic SOQL string] 98 | * @param orderedBindVars [Currently only an ID_SET_KEY, containing a list of sObject Ids to scope the query] 99 | * @return [List of dynamically queried SObjects scoped by some BIND_VAR] 100 | */ 101 | private static List getSObjectDataFromQueryString(String queryString, Object orderedBindVars) { 102 | Set idSet = new Set(); 103 | System.debug('getSObjectDataFromQueryString orderedBindVars '+orderedBindVars); 104 | 105 | Map reconstructedBindVars = (Map) orderedBindVars; 106 | 107 | if (reconstructedBindVars.get(ID_SET_KEY) != null) { 108 | List idList = (List) JSON.deserialize( 109 | JSON.serialize( 110 | reconstructedBindVars.get(ID_SET_KEY) 111 | ), 112 | List.class 113 | ); 114 | for (String sObjectId : idList) { 115 | idSet.add(sObjectId.trim()); 116 | } 117 | } 118 | try { 119 | return Database.query(queryString); 120 | } catch (Exception e) { 121 | throw new AuraHandledException(e.getMessage()); 122 | } 123 | } 124 | 125 | /** 126 | * Creates lightning:datatable ready tableColumns using the queryString and the queried object's schema. 127 | * 128 | * @param queryString [Dynamic SOQL String, to parse out fields] 129 | * @param queriedSObject [To grab full schema of fields, primarily for labels] 130 | * @return [List of individual tableColumn, i.e. tableColumns] 131 | */ 132 | private static List> getColumnData(String queryString, SObject queriedSObject) { 133 | String soqlFields = queryString.subString(queryString.indexOfIgnoreCase('select') + 6, queryString.indexOfIgnoreCase('from')).trim(); 134 | List soqlColumns = soqlFields.split('[,]{1}[\\s]?'); // sanitizes the spacing between commas 135 | List> tableColumns = new List>(); 136 | Map fieldMap = queriedSObject.getSObjectType().getDescribe().fields.getMap(); 137 | 138 | for (String fieldName : soqlColumns){ 139 | Schema.DescribeFieldResult field; 140 | Map fieldColumn = new Map(); 141 | 142 | // History tables have this field, ignore this one 143 | if (fieldname == 'created') { 144 | continue; 145 | } 146 | // Handles parent relationships, to a degree 147 | if (fieldName.contains('.')) { 148 | System.debug(fieldName); 149 | String parentReference = fieldName.contains('__r') 150 | ? fieldName.substringBeforeLast('__r.') + '__c' // custom objects 151 | : fieldName.substringBeforeLast('.') + 'Id'; // standard objects typical schema 152 | Schema.SObjectType referenceTo = fieldMap.get(parentReference).getDescribe().getReferenceTo().get(0); 153 | field = referenceTo.getDescribe().fields.getMap().get(fieldName.substringAfterLast('.')).getDescribe(); 154 | } else { 155 | field = fieldMap.get(fieldName).getDescribe(); 156 | } 157 | System.debug('getColumnData field info: '+fieldName+' : '+field.getType()); 158 | // Minor validations 159 | if ( 160 | field.getType() == Schema.DisplayType.ID // IDs are usually keyFields, so we skip display of this 161 | || field.getType() == Schema.DisplayType.Reference // References are lookups, need granular formatting so left to UI implementation 162 | || !field.isAccessible() // Respect FLS 163 | ) { 164 | continue; 165 | } 166 | // Handles parent relationships, to a degree 167 | if (fieldName.contains('.')) { 168 | fieldColumn.put('label', fieldName.substringBeforeLast('.') +' '+ field.getLabel()); 169 | fieldColumn.put('fieldName', fieldName.replace('.', '_')); // handled clientside by DataTableServiceHelper.js 170 | } else { 171 | fieldColumn.put('label', field.getLabel()); 172 | fieldColumn.put('fieldName', fieldName); 173 | } 174 | // Final assembly 175 | fieldColumn.put('type', DISPLAY_TYPE_TO_DATATABLE_TYPE_MAP.get(field.getType())); 176 | tableColumns.add(fieldColumn); 177 | } 178 | return tableColumns; 179 | } 180 | 181 | } -------------------------------------------------------------------------------- /src/classes/DataTableService.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/flexipages/Service_Components_Hello_World.flexipage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AccountSelector 6 | 7 | 8 | ContactDatatable 9 | 10 | 11 | PlatformEventListener 12 | 13 | region1 14 | Region 15 | 16 | 17 | region2 18 | Region 19 | 20 | 21 | region3 22 | Region 23 | 24 | Service Components Hello World 25 | 28 | AppPage 29 | 30 | -------------------------------------------------------------------------------- /src/flexipages/Service_Components_UtilityBar.flexipage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | utilityItems 5 | Region 6 | 7 | Service Components UtilityBar 8 | 11 | UtilityBar 12 | 13 | -------------------------------------------------------------------------------- /src/flows/Contacts_All.flow: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | myVariable_waitStartTimeAssignment 5 | 6 | 0 7 | 0 8 | 9 | myVariable_waitStartTimeVariable 10 | Assign 11 | 12 | $Flow.CurrentDateTime 13 | 14 | 15 | 16 | isChangedDecision2_myRule_1_MailingCity 17 | 18 | 19 | 20 | isChangedDecision2_myRule_1_MailingCity 21 | 22 | 0 23 | 0 24 | 25 | isChangedDecision3_myRule_1_MailingCountry 26 | 27 | default 28 | 29 | isChangedRule_2_myRule_1_MailingCity 30 | and 31 | 32 | myVariable_old 33 | IsNull 34 | 35 | false 36 | 37 | 38 | 39 | myVariable_old.MailingCity 40 | NotEqualTo 41 | 42 | myVariable_current.MailingCity 43 | 44 | 45 | 46 | isChangedDecision3_myRule_1_MailingCountry 47 | 48 | 49 | 50 | 51 | 52 | isChangedDecision3_myRule_1_MailingCountry 53 | 54 | 0 55 | 0 56 | 57 | isChangedDecision4_myRule_1_MailingState 58 | 59 | default 60 | 61 | isChangedRule_3_myRule_1_MailingCountry 62 | and 63 | 64 | myVariable_old 65 | IsNull 66 | 67 | false 68 | 69 | 70 | 71 | myVariable_old.MailingCountry 72 | NotEqualTo 73 | 74 | myVariable_current.MailingCountry 75 | 76 | 77 | 78 | isChangedDecision4_myRule_1_MailingState 79 | 80 | 81 | 82 | 83 | 84 | isChangedDecision4_myRule_1_MailingState 85 | 86 | 0 87 | 0 88 | 89 | isChangedDecision5_myRule_1_MailingStreet 90 | 91 | default 92 | 93 | isChangedRule_4_myRule_1_MailingState 94 | and 95 | 96 | myVariable_old 97 | IsNull 98 | 99 | false 100 | 101 | 102 | 103 | myVariable_old.MailingState 104 | NotEqualTo 105 | 106 | myVariable_current.MailingState 107 | 108 | 109 | 110 | isChangedDecision5_myRule_1_MailingStreet 111 | 112 | 113 | 114 | 115 | 116 | isChangedDecision5_myRule_1_MailingStreet 117 | 118 | 0 119 | 0 120 | 121 | isChangedDecision6_myRule_1_MailingPostalCode 122 | 123 | default 124 | 125 | isChangedRule_5_myRule_1_MailingStreet 126 | and 127 | 128 | myVariable_old 129 | IsNull 130 | 131 | false 132 | 133 | 134 | 135 | myVariable_old.MailingStreet 136 | NotEqualTo 137 | 138 | myVariable_current.MailingStreet 139 | 140 | 141 | 142 | isChangedDecision6_myRule_1_MailingPostalCode 143 | 144 | 145 | 146 | 147 | 148 | isChangedDecision6_myRule_1_MailingPostalCode 149 | 150 | 0 151 | 0 152 | 153 | myDecision 154 | 155 | default 156 | 157 | isChangedRule_6_myRule_1_MailingPostalCode 158 | and 159 | 160 | myVariable_old 161 | IsNull 162 | 163 | false 164 | 165 | 166 | 167 | myVariable_old.MailingPostalCode 168 | NotEqualTo 169 | 170 | myVariable_current.MailingPostalCode 171 | 172 | 173 | 174 | myDecision 175 | 176 | 177 | 178 | 179 | 180 | 181 | index 182 | 183 | 0.0 184 | 185 | 186 | myDecision 187 | 188 | 50 189 | 0 190 | default 191 | 192 | myRule_1 193 | or 194 | 195 | 196 | inputDataType 197 | 198 | Boolean 199 | 200 | 201 | 202 | leftHandSideType 203 | 204 | String 205 | 206 | 207 | 208 | operatorDataType 209 | 210 | String 211 | 212 | 213 | 214 | rightHandSideType 215 | 216 | Boolean 217 | 218 | 219 | isChangedRule_2_myRule_1_MailingCity 220 | EqualTo 221 | 222 | true 223 | 224 | 225 | 226 | 227 | inputDataType 228 | 229 | Boolean 230 | 231 | 232 | 233 | leftHandSideType 234 | 235 | String 236 | 237 | 238 | 239 | operatorDataType 240 | 241 | String 242 | 243 | 244 | 245 | rightHandSideType 246 | 247 | Boolean 248 | 249 | 250 | isChangedRule_3_myRule_1_MailingCountry 251 | EqualTo 252 | 253 | true 254 | 255 | 256 | 257 | 258 | inputDataType 259 | 260 | Boolean 261 | 262 | 263 | 264 | leftHandSideType 265 | 266 | String 267 | 268 | 269 | 270 | operatorDataType 271 | 272 | String 273 | 274 | 275 | 276 | rightHandSideType 277 | 278 | Boolean 279 | 280 | 281 | isChangedRule_4_myRule_1_MailingState 282 | EqualTo 283 | 284 | true 285 | 286 | 287 | 288 | 289 | inputDataType 290 | 291 | Boolean 292 | 293 | 294 | 295 | leftHandSideType 296 | 297 | String 298 | 299 | 300 | 301 | operatorDataType 302 | 303 | String 304 | 305 | 306 | 307 | rightHandSideType 308 | 309 | Boolean 310 | 311 | 312 | isChangedRule_5_myRule_1_MailingStreet 313 | EqualTo 314 | 315 | true 316 | 317 | 318 | 319 | 320 | inputDataType 321 | 322 | Boolean 323 | 324 | 325 | 326 | leftHandSideType 327 | 328 | String 329 | 330 | 331 | 332 | operatorDataType 333 | 334 | String 335 | 336 | 337 | 338 | rightHandSideType 339 | 340 | Boolean 341 | 342 | 343 | isChangedRule_6_myRule_1_MailingPostalCode 344 | EqualTo 345 | 346 | true 347 | 348 | 349 | 350 | myRule_1_A1 351 | 352 | 353 | 354 | 355 | 356 | 357 | originalFormula 358 | 359 | NOW() 360 | 361 | 362 | formula_7_myRule_1_A1_7341905770 363 | DateTime 364 | NOW() 365 | 366 | 367 | 368 | originalFormula 369 | 370 | $User.Username 371 | 372 | 373 | formula_8_myRule_1_A1_4018484012 374 | String 375 | {!$User.Username} 376 | 377 | Contacts_All-1_InterviewLabel 378 | 379 | 380 | ObjectType 381 | 382 | Contact 383 | 384 | 385 | 386 | ObjectVariable 387 | 388 | myVariable_current 389 | 390 | 391 | 392 | OldObjectVariable 393 | 394 | myVariable_old 395 | 396 | 397 | 398 | TriggerType 399 | 400 | onAllChanges 401 | 402 | 403 | Workflow 404 | 405 | myRule_1_A1 406 | 407 | 100 408 | 200 409 | 410 | 411 | dataType 412 | 413 | DateTime 414 | 415 | 416 | 417 | isRequired 418 | 419 | false 420 | 421 | 422 | 423 | leftHandSideLabel 424 | 425 | Timestamp 426 | 427 | 428 | 429 | leftHandSideReferenceTo 430 | 431 | 432 | 433 | 434 | 435 | rightHandSideType 436 | 437 | Formula 438 | 439 | 440 | Timestamp__c 441 | 442 | formula_7_myRule_1_A1_7341905770 443 | 444 | 445 | 446 | 447 | dataType 448 | 449 | String 450 | 451 | 452 | 453 | isRequired 454 | 455 | false 456 | 457 | 458 | 459 | leftHandSideLabel 460 | 461 | Type 462 | 463 | 464 | 465 | leftHandSideReferenceTo 466 | 467 | 468 | 469 | 470 | 471 | rightHandSideType 472 | 473 | String 474 | 475 | 476 | Type__c 477 | 478 | MAILING FIELD(S) CHANGED 479 | 480 | 481 | 482 | 483 | dataType 484 | 485 | String 486 | 487 | 488 | 489 | isRequired 490 | 491 | false 492 | 493 | 494 | 495 | leftHandSideLabel 496 | 497 | Username 498 | 499 | 500 | 501 | leftHandSideReferenceTo 502 | 503 | 504 | 505 | 506 | 507 | rightHandSideType 508 | 509 | Formula 510 | 511 | 512 | Username__c 513 | 514 | formula_8_myRule_1_A1_4018484012 515 | 516 | 517 | Contact_DML__e 518 | 519 | myVariable_waitStartTimeAssignment 520 | Active 521 | 522 | myVariable_current 523 | SObject 524 | false 525 | true 526 | true 527 | Contact 528 | 529 | 530 | myVariable_old 531 | SObject 532 | false 533 | true 534 | false 535 | Contact 536 | 537 | 538 | myVariable_waitStartTimeVariable 539 | DateTime 540 | false 541 | false 542 | false 543 | 544 | $Flow.CurrentDateTime 545 | 546 | 547 | 548 | -------------------------------------------------------------------------------- /src/objects/Contact_DML__e.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | Deployed 4 | StandardVolume 5 | 6 | Timestamp__c 7 | false 8 | false 9 | false 10 | false 11 | 12 | false 13 | DateTime 14 | 15 | 16 | Type__c 17 | false 18 | false 19 | false 20 | false 21 | 22 | 255 23 | false 24 | Text 25 | false 26 | 27 | 28 | Username__c 29 | false 30 | false 31 | false 32 | false 33 | 34 | 255 35 | false 36 | Text 37 | false 38 | 39 | 40 | Contact DML 41 | 42 | -------------------------------------------------------------------------------- /src/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DataServiceCtrl 5 | DataTableService 6 | ApexClass 7 | 8 | 9 | AccountSelector 10 | CaseDatatable 11 | ContactAddressForm 12 | ContactDatatable 13 | DataService 14 | DataTableService 15 | EventService 16 | MessageService 17 | PlatformEventListener 18 | QuickUpdateService 19 | ServiceAppEvent 20 | ServiceCompEvent 21 | ServiceRecordEvent 22 | modalFooter 23 | onMessage 24 | AuraDefinitionBundle 25 | 26 | 27 | Service_Components 28 | CustomApplication 29 | 30 | 31 | Contact_DML__e 32 | CustomObject 33 | 34 | 35 | SC_Sample_App 36 | CustomTab 37 | 38 | 39 | Service_Components_Hello_World 40 | Service_Components_UtilityBar 41 | FlexiPage 42 | 43 | 44 | Contacts_All 45 | Flow 46 | 47 | 48 | Admin 49 | Profile 50 | 51 | 44.0 52 | -------------------------------------------------------------------------------- /src/profiles/Admin.profile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Service_Components 5 | false 6 | true 7 | 8 | 9 | DataServiceCtrl 10 | true 11 | 12 | 13 | DataTableService 14 | true 15 | 16 | false 17 | 18 | false 19 | Contact_DML__e.Timestamp__c 20 | false 21 | 22 | 23 | false 24 | Contact_DML__e.Type__c 25 | false 26 | 27 | 28 | false 29 | Contact_DML__e.Username__c 30 | false 31 | 32 | 33 | true 34 | true 35 | true 36 | true 37 | true 38 | Contact_DML__e 39 | true 40 | 41 | 42 | SC_Sample_App 43 | DefaultOn 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/tabs/SC_Sample_App.tab: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created by Lightning App Builder 4 | Service_Components_Hello_World 5 | 6 | false 7 | Custom56: Bottle 8 | 9 | --------------------------------------------------------------------------------