https://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.png00Tamar Erlichhttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngTamar Erlich2020-10-23 06:47:492020-10-26 04:40:08Salesforce Record Automation Benchmarking by Luke Freeland
https://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.png00Tamar Erlichhttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngTamar Erlich2020-10-16 12:49:182020-10-18 12:02:33Use Flows to Track Unread Emails on Cases and Increase Case Management Efficiency by Vibhor Goel
I was recently tasked with a requirement to display a list of opportunity partners on a custom object related to opportunity. I planned to use a flow to get the opportunity partner records and then display them using the Datatablev2 Flow Screen Component available on this site. When I tried to create the flow, I encountered some issues: 1. The Opportunity Partners object is a legacy object and cannot be customized. I could not create formulas for getting the account name or link to the account 2. I could not use dot notation to get the account name from the account ID in the get records component in the flow 3. I could not use the partner account ID as a filter criteria on a new get records element to get the account records.
Not Possible
I knew I had to create a custom way to get the data I needed to display. Eric Smith referred me to his blog post on how to use Apex defined data types and an invocable action to create data for the datatable component. How to Use an Apex-Defined Object with my Datatable Flow Component I create my Apex defined data type for opportunity partners.
/**
* @author : Tamar Erlich
* @date : October 07, 2020
* @description : Wrapper class for opportunity partners
* Note : This class is called by the GetOpportunityPartnersAction.
* *************************************************
* <runTest><runTest>
* *************************************************
* @History
* -------
* VERSION | AUTHOR | DATE | DESCRIPTION
* 1.0 | Tamar Erlich | October 07, 2020 | Created
**/
public with sharing class OpportunityPartnersWrapper {
// @AuraEnabled annotation exposes the methods to Lightning Components and Flows
@AuraEnabled
public String accountName;
@AuraEnabled
public String partnerRole;
@AuraEnabled
public Boolean isPrimary;
@AuraEnabled
public String accountLink;
// Define the structure of the Apex-Defined Variable
public OpportunityPartnersWrapper(
String accountName,
String partnerRole,
Boolean isPrimary,
String accountLink
) {
this.accountName = accountName;
this.partnerRole = partnerRole;
this.isPrimary = isPrimary;
this.accountLink = accountLink;
}
// Required no-argument constructor
public OpportunityPartnersWrapper() {}
}
I then created an invocable action that received the opportunity and account IDs and returns a JSON string that can be displayed in my flow table.
/**
* @author : Tamar Erlich
* @date : October 07, 2020
* @description : Invocable method that given opportunityId and accountId, returns a list of opportunity partners to display on opportunity plan record page
* Note : This class is called by the Flow.
* *************************************************
* <runTest>GetOpportunityPartnersActionTest<runTest>
* *************************************************
* @History
* -------
* VERSION | AUTHOR | DATE | DESCRIPTION
* 1.0 | Tamar Erlich | October 07, 2020 | Created
**/
global with sharing class GetOpportunityPartnersAction {
// Expose this Action to the Flow
@InvocableMethod
global static List<Results> get(List<Requests> requestList) {
// initialize variables
Results response = new Results();
List<Results> responseWrapper = new List<Results>();
String errors;
String success = 'true';
String stringOutput;
List<OpportunityPartner> opportunityPartners = new List<OpportunityPartner>();
List<OpportunityPartnersWrapper> opportunityPartnersSet = new List<OpportunityPartnersWrapper>();
Set<Id> setOppIds = new Set<Id>();
Set<Id> setAccIDs = new Set<Id>();
Map<Id, List<OpportunityPartner>> mapOppOppPartners = new Map<Id, List<OpportunityPartner>>();
// create sets of Ids to use as bind variables in the query
for (Requests rq : requestList) {
setOppIds.add(rq.opportunityId);
setAccIds.add(rq.accountId);
}
// query all request opportunities and their partners and create a map with opportunityId as the key and a list of parrtners as the values
for (Opportunity opp : [
SELECT
Id,
(
SELECT AccountTo.Name, IsPrimary, Role
FROM OpportunityPartnersFrom
WHERE OpportunityId = :setOppIds AND AccountToId != :setAccIds
)
FROM Opportunity
]) {
mapOppOppPartners.put(opp.Id, opp.OpportunityPartnersFrom);
}
for (Requests curRequest : requestList) {
String accountId = curRequest.accountId;
String opportunityId = curRequest.opportunityId;
try {
if (accountId == null || opportunityId == null) {
throw new InvocableActionException(
'When using the GetOpportunityPartners action, you need to provide BOTH an Account Id AND an Opportunity Id'
);
}
if (!accountId.startsWith('001') || !opportunityId.startsWith('006')) {
throw new InvocableActionException(
'Invalid Account or Opportunity ID'
);
}
// get the list of opportunity partners matching the current opportunityId from the map
if (accountId != null && opportunityId != null) {
opportunityPartners = mapOppOppPartners.get(opportunityId);
}
// populate the Apex defined wrapper type with opportunity partner information
for (opportunityPartner op : opportunityPartners) {
OpportunityPartnersWrapper opw = new OpportunityPartnersWrapper();
opw.accountName = op.AccountTo.Name;
opw.partnerRole = op.Role;
opw.isPrimary = op.IsPrimary;
opw.accountLink = '/' + op.AccountToId;
opportunityPartnersSet.add(opw);
}
// Convert Record Collection to Serialized String
stringOutput = JSON.serialize(opportunityPartnersSet);
} catch (InvocableActionException e) {
System.debug('exception occured: ' + e.getMessage());
errors = e.getMessage();
success = 'false - custom exception occured';
} catch (exception ex) {
System.debug('exception occured: ' + ex.getMessage());
errors = ex.getMessage();
success = 'false - exception occured';
}
// Prepare the response to send back to the Flow
// Set Output Values
response.errors = errors;
response.successful = success;
response.outputCollection = opportunityPartnersSet;
response.outputString = stringOutput;
responseWrapper.add(response);
}
// Return values back to the Flow
return responseWrapper;
}
// Attributes passed in from the Flow
global class Requests {
@InvocableVariable(label='Input the Related OpportunityId' required=true)
global String opportunityId;
@InvocableVariable(label='Input the Opportunity\'s AccountId' required=true)
global String accountId;
}
// Attributes passed back to the Flow
global class Results {
@InvocableVariable
global String errors;
@InvocableVariable
global String successful;
@InvocableVariable
public List<OpportunityPartnersWrapper> outputCollection;
@InvocableVariable
public String outputString;
}
// custom exception class
global class InvocableActionException extends Exception {
}
}
/**
* @author : Tamar Erlich
* @date : October 07, 2020
* @description : Test class for GetOpportunityPartnersAction Invocable method
* Note :
* *************************************************
* <runTest>GetOpportunityPartnersActionTest<runTest>
* *************************************************
* @History
* -------
* VERSION | AUTHOR | DATE | DESCRIPTION
* 1.0 | Tamar Erlich | October 07, 2020 | created
**/
@IsTest
public with sharing class GetOpportunityPartnersActionTest {
@isTest
public static void opportunityPartnersFound(){
// initialize variables
List<GetOpportunityPartnersAction.Requests> requestList = new List<GetOpportunityPartnersAction.Requests>();
String accountId;
String opportunityId;
// create test account
List<Account> accounts = new List<Account>();
for (Integer j = 0; j < 1; j++) {
Account a = new Account(
Name = 'email' + j + '.com'
);
accounts.add(a);
}
insert accounts;
// create test opportunity
Date closeDate = System.today();
Opportunity testOpp;
testOpp = new Opportunity(
Name = 'test opp',
StageName = 'Open In-Progress',
Amount = 100,
CloseDate = closeDate
);
insert testOpp;
accountId = accounts[0].Id;
opportunityId = testOpp.Id;
// create test opportunity partner
OpportunityPartner testPartner;
testPartner = new OpportunityPartner(
OpportunityId = opportunityId,
AccountToId = accountId,
Role = 'Dealer',
IsPrimary = false
);
insert testPartner;
// prepare request for GetOpportunityPartnersAction
GetOpportunityPartnersAction.Requests request = new GetOpportunityPartnersAction.Requests();
request.accountId = accountId;
request.opportunityId = opportunityId;
requestList.add(request);
// run test and assert results
List <GetOpportunityPartnersAction.Results> results = GetOpportunityPartnersAction.get(requestList);
System.assertEquals(results[0].errors, null,'Errors not expected');
System.assertNotEquals(results[0].successful, 'false', 'Success expected');
System.assertEquals(true, results[0].outputCollection.size()>0, 'Opportunity Partners expected');
}
}
I then used my new action in the flow to get the opportunity partner records.
Invocable action
I then configured my datatable on the flow screen
Flow Screen
Some notes on the datatable configuration: 1. User Defined had to bet set to true 2. The Datatable Record String must be set to the variable returned from the action 3. The column field types must be set for all columns. I set the first column to the url type 4. My first column has special type attributes for displaying as a clickable link: 1:{label: { fieldName: accountName}, target: ‘_blank’} 5. Since this is for display only, I hid the checkbox columns 6. I used the new features available in v2.46 to apply an icon and title to the table
The View All link uses a formula to create the link
View All Formula
Here is the entire flow
And here is the end result
Opportunity Partners on custom object
This use case can be easily adapted to display other opportunity related information like opportunity team members or opportunity contact roles.
Trying to report on user login history using standard Salesforce reporting can be tricky. If you would like to see each users’s last login and his total login count for the last 6 months, sorted by login count, you have to use some external Excel manipulation. I was looking to get a simple report I could use to monitor login usage.
Simple Login Report
I was able to provide a nice solution using the new before save flow record trigger. The first step is to create a number field on the user object to hold the login count. Then create this simple before save flow. This flow updates the custom field on the User object. By incrementing a counter every time a login history is generated, each user shows a login count. Then, at any time, you can generate the report by simply reporting on your users.
User Login History FlowGet Login History RecordsConfirm records are returned (Null Check)Use the record count operator to assign the record count to the user fieldIf no records are returned, assign zero login count
This use case can be easily adapted to rollup any related records on other objects. I can think of several examples: count of opportunity contact roles on opportunity, count open cases on a contact, count of won opportunities on an account.
https://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.png00Tamar Erlichhttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngTamar Erlich2020-08-12 15:43:282020-08-12 16:39:53Flow Use Case - Use Before Save flow to update the total login count on the user record
With the advent of System-context screen flows I can’t help but think of the famous phrase ‘With great power comes great responsibility’. When running screen flows in system context, you could inadvertently grant Read or Edit access to a critical system field to the wrong audience. Let’s say you wanted to do some cool stuff like in Jen Lee’s recent post about Community User Management, and you want to ensure the person running the flow has the ‘Manage External User’ permission. Her flow does this check using a custom permission, which is totally fine, this just checks the system permissions directly.
All of that is possible with some straightforward SOQL queries against the Permission Set Assignment object. You may or may not be aware, but the PermissionSet object actually contains Profile permissions as well so it’s a one-stop shop for getting Field and System permissions!
Run an action to check a field permission
The following example will run you through a VERY basic scenario where a Flow presents a screen to update a given field and the flow is running in System context – God mode.
We let the user pick an Account Rating in a basic Flow screen with a picklist radio button
We run the ExecuteSOQL action to check if the running user has ‘Edit’ permission for the Account Rating field.
For older versions of ExecuteSOQL that return an ’empty’ collection we assign the count of the results to a number field
We then do a decision to see if the number of permissions assigned to the user is > 0.
We then either show a screen that says you cant edit it or we move on with updating the field.
Flow Overview
Most of you can probably handle the beginning and end – so I’ll provide some more color on the middle part that does the permission checking.
You’ll want to construct your query using a Plain Text text template variable:
SELECT AssigneeId,PermissionSetId,Permissionset.Name,Permissionset.Profile.Name FROM PermissionSetAssignment WHERE Assignee.Id= ‘{!formulaRunningUserID}’ AND PermissionSetId in (Select ParentId from FieldPermissions where SobjectType = ‘Account’ and Field = ‘Account.Rating’ and PermissionsEdit = true)
*WARNING* If you make any edits to the text template variable, a Flow bug will revert the template back to rich text and it will break the action! Make sure you revert it to Plain Text after every edit.
Next up is to make the ExecuteSOQL action:
Here’s an example result from the action above that returns the permission granted by the System Admin profile when run as myself.
(For older versions of ExecuteSOQL only) You’ll then want to assign the results to a number since older versions of ExecuteSOQL return an empty collection if no results are found. Make sure you set the default value to 0 when making the variable.
Use an ISNULL check if you’re using the latest and greatest.
Create your decision and you’re done!
Checking multiple fields
You could of course extend this to multiple fields in one query by modifying the SOQL query, for example:
SELECT AssigneeId,PermissionSetId,Permissionset.Name,Permissionset.Profile.Name FROM PermissionSetAssignment WHERE Assignee.Id= ‘0051I000000UB4LQAW’ AND PermissionSetId in (Select ParentId from FieldPermissions where SobjectType = ‘Account’ and Field in (‘Account.Rating’,’Account.Type’) and PermissionsEdit = true)
System permission checks
You can also check for specific system permissions! Let’s say you wanted a Flow embedded on the Case layout that manages the Contact’s community user record. The query below could be used to check if the user handling the case has permissions to manage the external user account:
SELECT AssigneeId,PermissionSetId,Permissionset.Name,Permissionset.Profile.Name FROM PermissionSetAssignment WHERE Assignee.Id= ‘{!formulaRunningUserId}’ AND PermissionSetId in (Select Id from PermissionSet where PermissionsManagePartners = true)
https://unofficialsf.com/wp-content/uploads/2020/06/FlowOverview.png497955Adam Whitehttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngAdam White2020-06-09 10:00:592020-06-09 10:19:31Tutorial: Check field and system permissions in screen flows when using System Context using ExecuteSOQL
GORAV SETH demonstrates how you can use ISCHANGED and PRIORVALUE in Before Save Flows Check it out
https://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.png00Tamar Erlichhttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngTamar Erlich2020-05-12 12:46:322020-05-12 12:46:33How to use ISCHANGED and PRIORVALUE in Before Save Flows by GORAV SETH
ABOUT This is a Lightning Component that plays a sound effect. It can be used on Flow Screens and Lightning Pages. In order to play a sound, you have to upload the sound file as a Static Resource and then enter the name of the file to the “Sound File” parameter.
PARAMETERS Sound File – Enter the name of the sound file.
CONSIDERATIONS This component loads the sound file from Static Resources and plays it. So it is recommended to use smaller file sizes, otherwise it might take some time to load the file.
HOW TO USE 1- Upload the sound file as a Static Resource.
2- Add “SoundCMP” to your Flow Screen. 3- Enter the name of the sound file to the parameter called “Sound File”.
https://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.png00Yumi Ibrahimzadehttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngYumi Ibrahimzade2020-03-06 07:25:082020-03-06 07:39:29A Flow Screen Component that plays sounds
Have you ever wanted to add your own logging to your flows? I’m not talking about the system logs, but rather creating your own log entries at specific places in your flow. I’ve had a couple of instances, where I could really use this. Both for debugging as well as simply tracing whether an auto-launched flow has executed. After having reinvented the wheel a few times, I got tired of it and decided to create something that is more reusable, and it has proven its value already more than once.
What I’ve done is this… I’ve created a custom object, a number of flows and a few tools to manage it all. You can add this flow as a subflow to any of your own flows. This step will create a new record in the custom object on which you can report.
This Quip document contains all the details and the link to the managed package.
Flow Log Step Settings
https://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.png00Wouter Duprehttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngWouter Dupre2020-03-06 07:04:042020-03-06 07:04:05Custom Logging in Flows
https://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.png00Tamar Erlichhttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngTamar Erlich2020-03-02 09:59:152020-03-02 09:59:17Update N Records in Flow Without a Loop by Jessie Rymph
The first post in a new series on the Salesforce admin blog demonstrates a novel way to launch auoto-launched flows from a formula field on a report. This has several very interesting use cases Check it out
https://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.png00Tamar Erlichhttps://unofficialsf.com/wp-content/uploads/2020/05/USFlogo2-300x111.pngTamar Erlich2020-02-27 06:00:192020-02-27 06:00:20Launch a flow from a formula field