Retire of Permission on Profiles

If you are working as a Salesforce admin/developer you've probably heard somewhere that Salesforce is planning to make a significant change regarding profiles. The main idea is that most of the permissions will no longer be managed through the profiles, but instead through a Permission Set. Profile will still be used, for things like login hours, session settings and probably some additional items. 

Currently, the plan is for Spring '26 release.


It can be said that there is time until this change and there is no need to rush, however this is major change with significant impact and we can notice in the latest versions of Salesforce that some new features provide the tools to support the transition. 

For example: 

  • Field-Level Security for Permission Sets during Field Creation

 This new feature allows admin when creating fields to set                 permissions for a premium set, instead of a profile.

  • User Access Policies (Beta)

Allow to define a set of permissions for users and assigning              them based on criteria.

You can activate those new features, along with other, on the User Management Settings



Of course, as part of the transition, admin might need to do some adjustment with their current implementation. If you work according to best practices, you may not need to make any changes, because for quite a long time Salesforce has been recommending to manage the permissions for fields and objects using a permission set,

If not, you'll might need to make a few adjustments to migrate your implementation. Like creating new Permission Sets, setup their content and assign them to users.


The application Permissions Helper is a free app that allows managing profile and permission sets more easily, and facilitates the process when it is required to work with bulk operations.

A special feature has been added to the application to help with the process of shifting to a Permission Sets usage. It allows you to copy the permissions from a profile to a Permission Set with a few steps:

1. Manually create new Permission Set.

2. Go to the tab Update Profiles/Permission Sets.

3. Click the button Copy Profile to Permission Set.

4. Select the relevant Profile and Permission Set and click Copy.


Here is a quick demo



Automatically Refresh Lightning Record Page




Salesforce implementers/developers often encounter a case where several processes, some of them background processes, update the data that the users are working on, and we are looking for the best option to keep the users synced.


Note that this does not require treatment, since if the user tries to update data that has already changed, the system will alert the user of the changes when the save attempt is made. However, it would be nice if the updated data could be reflected to the user in a simple and automatic way.


The good news, it is possible to do this with a very simple web component that we add on the record page and every X seconds it will notify the page to check if there have been any changes.


Here how it works.

Create new Lightning Web Component. 

The xml file will note that it is meant for record page.

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>59.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Refresh Page</masterLabel>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>

    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <property name="refreshInterval" type="Integer" label="Refresh Interval"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>



The html is optional, we can show counter down, something else or show nothing.

<template>
    Next Refresh:  {secondsLeft}
</template>



The java script file set repeated interval using the method setInternval. Every second the component will reduce 1 second from its count. Once it reach to zero, it will reset the counter and use the method notifyRecordUpdateAvailableThe method, as the name imply, notify to the page that there might be other changes and it should take the appropriate actions to reflect those.

import { LightningElement, track, api } from 'lwc';
import { notifyRecordUpdateAvailable } from 'lightning/uiRecordApi';

export default class RefreshRecordPage extends LightningElement {
    @api refreshInterval = 5;
    @api recordId;
    @track secondsLeft;

    connectedCallback(){
        this.secondsLeft = this.refreshInterval;

        setInterval(() => {
            this.secondsLeft = this.secondsLeft - 1;

            if(this.secondsLeft < 0){

                notifyRecordUpdateAvailable([{recordId: this.recordId}]);

                this.secondsLeft = this.refreshInterval;
            }
        }, 1000);
    }
}



After deploying the component, last thing is to place it on the lightning record page that you want to refresh.


Width Detection of the Lightning Web Component

Dealing with screens of different sizes on almost every platform can be very challenging.
In Salesforce, when developing custom components, we need to consider not only that the component will be displayed on different screens but also that it might be in different areas of the screen. For example, if we develop a component for lightning pages, it can be added as the main region, left side, or right region.

In most cases, we would prefer to let the Salesforce shell take care of it for us, and indeed, most of the time, if we use the right CSS elements, it should work fine.
Still, there will be times when we need to use a different display for different screen widths.

In the following example, you can see an implementation of a calendar that displays seven days across the screen width. When the screen is too narrow, it reduces the width of each cell, but at a certain level, the width will become so narrow that it won't be possible to insert anything in the cell, so the implementation is that on narrow screens, the display will be slightly different.



How can this be done? How can we identify the width of the screen, or rather, the width of our component?

First, note that if we are only interested in the size of the screen, it is possible and recommended to use a CSS media query. This allows us to set different styling CSS based on the type and size of the screen.
However, in this case, we want to use the component size, so we will use other solutions.


1. A relatively simple solution is to use the variable that Salesforce prepared for us: flexipageRegionWidth.
The possible values ​​are SMALL, MEDIUM, and LARGE and it is indicate the width of our component, not the width of the screen.
In order to use it, all we need to do is set api variable with this name, and then we can build logic based on that.


@api flexipageRegionWidth;

get isnarrowscreen(){
    return this.flexipageRegionWidth == 'SMALL';
}

<template lwc:if={isnarrowscreen}>
	narrow
</template>

<template lwc:else>
	wide
</template>

After adding a component with this code twice to a lightning page, this will be the result




  • In the same area, it is worth noting that it is also possible to know the type of user's device (desktop, table or phone) by importing the formFactor model. 

2. In some cases, we will want higher accuracy and not necessarily sizes like SMALL, MEDIUM, and LARGE. There are several variables that can be accessed in javascript that will allow us to read the width of the screen, but the tricky part is detecting the width of our component. One of the techniques for this purpose is to measure the distance in one of the UI components.
For example, I added in the component variable compWidth and calculate it based on the right-left of the element "main".

@api flexipageRegionWidth;
compWidth;

get isnarrowscreen(){
	return this.flexipageRegionWidth == 'SMALL';
}

connectedCallback(){
	this.setCompWidth();
}

setCompWidth(){
	let containerElem = this.template.querySelector(".main");

	if(containerElem){
		this.compWidth = containerElem.offsetWidth;   
	}
}

<template>
    <div class="main">
        <template lwc:if={isnarrowscreen}>
            narrow
        </template>

        <template lwc:else>
            wide
        </template>
        <br/>
        width: {compWidth}
    </div>

</template>


The result:





3. Notice that in the examples above, there is no treatment in cases where the width changes. For example, when the user expands or narrows the window, one option for handling that is to set a resize event. This way, any time the window size is changed, we will invoke the method that recalculates the dimensions.

In the javascript code, I will add one line that listens to the resize event:

@api flexipageRegionWidth;
compWidth;

get isnarrowscreen(){
	return this.flexipageRegionWidth == 'SMALL';
}

connectedCallback(){
	this.setCompWidth();

	window.addEventListener('resize', ()=> this.setCompWidth());
}

setCompWidth(){
	let containerElem = this.template.querySelector(".main");

	if(containerElem){
		this.compWidth = containerElem.offsetWidth;   
} }



The result:






Finally, it is worth noting that messing with the width and different displays increases the complexity of the development, and in most cases, it is recommended to let the platform determine the correct display for the components. When we decide that there is a need for such customization, we should take into account that we are taking responsibility for an important part that may require more maintenance.

Native App for Managing Profiles/Permission Sets




A few weeks ago, I wrote about the options for managing field permissions for Profiles. I tested several tools for these operations, as well as checking the capabilities to update such permissions using native Apex code by writing a small tool.

One of the findings is that such Apex code is difficult to maintain with many different validation end cases. Nevertheless, the tool was effective and good in many use cases. For that reason, I added a few more functionalities, improved the UI and the error handling, and created an app based on it that can be easily installed in any Salesforce environment.

The app, Permission Helper, is completely free, the use is simple, and of course there are all the advantages of native code: everything runs on the Salesforce platform, so it's faster and safer.


Usage:

  • Go to the tab Update Profiles/Permission Sets 
  • Choose one of the two options: Update fields permissions/Update objects permission
  • Select the Profiles/Permission Sets as well as the fields or objects to update.
  • Modify the permissions.
  • Save


It is important to note that the application does not manage dependencies between permissions but simply tries to save the settings that were made, and in the case of any violation of a setting, an error will be displayed to the user. For example, if a certain permission is required to get update permission on the Lead object, when you try to give such permission through the standard Salesforce UI, it will notify you about the missing permission and will offer to add it; the app, on the other hand, will simply display an error message saying that the relevant permission is missing. 

Building Reusable Process that Generate Test Data

When we create a new org in many cases it is empty and we need some test data.

In a number of previous instances, I required a large number of Leads entries, therefore I wrote a script to produce 100 Leads using random data. Later on, though, I found that I wanted something similar but for a different data type, so I decided to turn my script into a reusable method. 

In this article, I'll go over the conversion process step-by-step, along with the problems it encountered and how it was resolved, as well as a few potential shortcomings.


On parts of code that we occasionally run, such data fix, a similar process can be carried out. However, the benefit versus cost factors must be taken into account. How different each run from the previous one is, how frequently we use it, and of course, how much it cost to develop.


Here is my initial script, which generated 100 records for leads. Everything is plainly hardcoded, therefore this script is solely useful for producing Leads, as you can see.

List<Lead> newLeads = new List<Lead>();

List<String> ratingList = new List<String>{'Hot', 'Warm', 'Cold'};
Integer totalRatings = ratingList.size();

List<String> indList = new List<String>{'Agriculture', 'Apparel', 'Banking', 'Education', 'Consulting'};
Integer totalnd = ratingList.size();

List<String> countryList = new List<String>{'Israel', 'Spain', 'USA', 'Italy', 'Sweden', 'Brazil', 'Chile'};
Integer totaCountry = ratingList.size();

for(Integer index = 0; index < 100; index ++){
	newLeads.add(new Lead(
		FirstName = 'Test',
		LastName = 'Lead ' + index,
		Company = 'DemoLead ' + index,
		Email = 'test.lead' + index + '@test.com',
		Rating = ratingList.get(Integer.valueof((Math.random() * totalRatings))),
		Industry = indList.get(Integer.valueof((Math.random() * totalnd))),
		Status = 'Working - Contacted',
		LeadSource = 'Web',
		Country = countryList.get(Integer.valueof((Math.random() * totaCountry)))));
}

insert newLeads;


From Script to Method

I will first create an Apex Class with a method and add the script to it. Obviously, the process isn't truly random at the present, but we'll take care of that.

public with sharing class DataCreator{
	public static void generateRandomData(){
	
		List<Lead> newLeads = new List<Lead>();

		List<String> ratingList = new List<String>{'Hot', 'Warm', 'Cold'};
		Integer totalRatings = ratingList.size();

		List<String> indList = new List<String>{'Agriculture', 'Apparel', 'Banking', 'Education', 'Consulting'};
		Integer totalnd = ratingList.size();

		List<String> countryList = new List<String>{'Israel', 'Spain', 'USA', 'Italy', 'Sweden', 'Brazil', 'Chile'};
		Integer totaCountry = ratingList.size();

		for(Integer index = 0; index < 100; index ++){
			newLeads.add(new Lead(
				FirstName = 'Test',
				LastName = 'Lead ' + index,
				Company = 'DemoLead ' + index,
				Email = 'test.lead' + index + '@test.com',
				Rating = ratingList.get(Integer.valueof((Math.random() * totalRatings))),
				Industry = indList.get(Integer.valueof((Math.random() * totalnd))),
				Status = 'Working - Contacted',
				LeadSource = 'Web',
				Country = countryList.get(Integer.valueof((Math.random() * totaCountry)))));
		}

		insert newLeads;
	}
}


Since the code should be able to construct any data type, we cannot use the object name Lead in the method. Instead, we will change List<Lead> to ListSObject>.


public with sharing class DataCreator{
	public static void generateRandomData(){
	
		List<sObject> newRecords = new List<sObject>();

		...

		insert newRecords;
	}
}



Each object has a unique set of fields, it should not refer to them by name. Therefore, we will add the object type as parameter and use the describe method to access its fields.

A list of the mandatory fields for the object type should also be kept because each record must have data for them.


public with sharing class DataCreator{
	public static void generateRandomData(String objectType){
	
		List<sObject> newRecords = new List<sObject>();

		//Get all object fields
		SObjectType objectRef = Schema.getGlobalDescribe().get(objectType);
		Map<String, Schema.SObjectField> fieldMap = objectRef.getDescribe().fields.getMap();
		
		//Store list of require fields
		List<Schema.DescribeFieldResult> requireFields = new List<Schema.DescribeFieldResult>();
		
		for(Schema.SObjectField sField : fieldMap.values()){
			Schema.DescribeFieldResult dfr = sField.getDescribe();
			
			if(! dfr.isNillable() && dfr.isCreateable()){
				requireFields.add(dfr);
			}
		}
		
		

		insert newRecords;
	}
}


Let's build some records next. For this, I included a parameter that specifies how many records it should generate, as well as a loop that creates the new records with the require fields.


public with sharing class DataCreator{
	public static void generateRandomData(
		String objectType, 
		Integer totalRecords){
	
		List<sObject> newRecords = new List<sObject>();

		//Get all object fields
		SObjectType objectRef = Schema.getGlobalDescribe().get(objectType);
		Map<String, Schema.SObjectField> fieldMap = objectRef.getDescribe().fields.getMap();
		
		//Store list of require fields
		List<Schema.DescribeFieldResult> requireFields = new List<Schema.DescribeFieldResult>();
		
		for(Schema.SObjectField sField : fieldMap.values()){
			Schema.DescribeFieldResult dfr = sField.getDescribe();
			
			if(! dfr.isNillable() && dfr.isCreateable()){
				requireFields.add(dfr);
			}
		}
		
		for(Integer index = 0; index < totalRecords; index ++){
			SObject newRecord = objectRef.newSobject();
		
			for(Schema.DescribeFieldResult dfr : requireFields){
				if(dfr.getType() == Schema.DisplayType.Picklist || dfr.getType() == Schema.DisplayType.MultiPicklist){                      
					newRecord.put(dfr.getName(), dfr.getPicklistValues().get(Integer.valueof((Math.random() * dfr.getPicklistValues().size()))).getValue());
				}
				else if(dfr.getType() == Schema.DisplayType.String || dfr.getType() == Schema.DisplayType.TextArea || dfr.getType() == Schema.DisplayType.URL){
					newRecord.put(dfr.getName(), 'test1');
				}
				else if(dfr.getType() == Schema.DisplayType.Integer || dfr.getType() == Schema.DisplayType.Long || dfr.getType() == Schema.DisplayType.Double){
					newRecord.put(dfr.getName(), 1);
				}
				else if(dfr.getType() == Schema.DisplayType.Date || dfr.getType() == Schema.DisplayType.DateTime){
					newRecord.put(dfr.getName(), System.today());
				}
				else if(dfr.getType() == Schema.DisplayType.Boolean){
					newRecord.put(dfr.getName(), false);
				}
				else if(dfr.getType() == Schema.DisplayType.REFERENCE) {
				
					set<String> sTypes = new set<String>();
					
					for(Schema.sObjectType refTo : dfr.getReferenceTo()){
						sTypes.add(refTo.getDescribe().getName());
					}
					
					if(sTypes.contains('User')){
						newRecord.put(dfr.getName(), UserInfo.getUserId());
					}
					else if(sTypes.contains('Profile')){
						//Special logic for profile as we cannot create profiles will use the profile of the running user
						newRecord.put(dfr.getName(), UserInfo.getProfileId());
					}
				}
				else{
					//Unsupported type is require - need to improve the code
					System.debug('Require field is not populated: ' + dfr.getName() + ' ' + dfr.getType());
				}
			}
			
			newRecords.add(newRecord);
		}
		
		

		insert newRecords;
	}
}


You can see that the code check for the type of each field and sets a dummy value accordingly.  Should also notice that not all field types are supported.


It may already be used with the following commands to generate some test data:

DataCreator.generateRandomData('Lead', 5);

DataCreator.generateRandomData('Account', 50);


While it might work for a few cases, it is insufficient for most cases.

Typical use cases that are missing:

  • Fill in any more fields that are not required.
  • Set particular value(s) for particular fields


I used an inner class type that will hold settings on how to fill up particular fields in order to get greater flexibility.

For each field that we want to populate, we will send the field name as well as list of optional values. The method will randomly set one of the value for each record.


public class FieldSettings{
	private String fieldName;
	private List<object> optionalValues;
	
	public FieldSettings(String fieldName, List<object> optionalValues){
		this.fieldName = fieldName;, 
		this.optionalValues = optionalValues;
	}
}


We can now improve the function to accept a list of FieldSettings as an parameter and utilize it when creating records.

Also notice that the code now set a require field only if it is empty. Meaning, it was not populated by the field settings.


public with sharing class DataCreator{
	public static void generateRandomData(
		String objectType, 
		Integer totalRecords,
		List<FieldSettings> fieldsList){
	
		List<sObject> newRecords = new List<sObject>();

		//Get all object fields
		SObjectType objectRef = Schema.getGlobalDescribe().get(objectType);
		Map<String, Schema.SObjectField> fieldMap = objectRef.getDescribe().fields.getMap();
		
		//Store list of require fields
		List<Schema.DescribeFieldResult> requireFields = new List<Schema.DescribeFieldResult>();
		
		for(Schema.SObjectField sField : fieldMap.values()){
			Schema.DescribeFieldResult dfr = sField.getDescribe();
			
			if(! dfr.isNillable() && dfr.isCreateable()){
				requireFields.add(dfr);
			}
		}
		
		for(Integer index = 0; index < totalRecords; index ++){
			SObject newRecord = objectRef.newSobject();
			
			for(FieldSettings fSettings : fieldsList){
				newRecord.put(fSettings.fieldName, fSettings.optionalValues.get(Integer.valueof((Math.random() * fSettings.optionalValues.size()))));
			}
		
			for(Schema.DescribeFieldResult dfr : requireFields){
				if(newRecord.get(dfr.getName()) == null){
			
					if(dfr.getType() == Schema.DisplayType.Picklist || dfr.getType() == Schema.DisplayType.MultiPicklist){                      
						newRecord.put(dfr.getName(), dfr.getPicklistValues().get(Integer.valueof((Math.random() * dfr.getPicklistValues().size()))).getValue());
					}
					else if(dfr.getType() == Schema.DisplayType.String || dfr.getType() == Schema.DisplayType.TextArea || dfr.getType() == Schema.DisplayType.URL){
						newRecord.put(dfr.getName(), 'test1');
					}
					else if(dfr.getType() == Schema.DisplayType.Integer || dfr.getType() == Schema.DisplayType.Long || dfr.getType() == Schema.DisplayType.Double){
						newRecord.put(dfr.getName(), 1);
					}
					else if(dfr.getType() == Schema.DisplayType.Date || dfr.getType() == Schema.DisplayType.DateTime){
						newRecord.put(dfr.getName(), System.today());
					}
					else if(dfr.getType() == Schema.DisplayType.Boolean){
						newRecord.put(dfr.getName(), false);
					}
					else if(dfr.getType() == Schema.DisplayType.REFERENCE) {
					
						set<String> sTypes = new set<String>();
						
						for(Schema.sObjectType refTo : dfr.getReferenceTo()){
							sTypes.add(refTo.getDescribe().getName());
						}
						
						if(sTypes.contains('User')){
							newRecord.put(dfr.getName(), UserInfo.getUserId());
						}
						else if(sTypes.contains('Profile')){
							//Special logic for profile as we cannot create profiles will use the profile of the running user
							newRecord.put(dfr.getName(), UserInfo.getProfileId());
						}
					}
					else{
						//Unsupported type is require - need to improve the code
						System.debug('Require field is not populated: ' + dfr.getName() + ' ' + dfr.getType());
					}
				}
			}
			
			newRecords.add(newRecord);
		}
		
		

		insert newRecords;
	}
	
	
	public class FieldSettings{
		private String fieldName;
		private List<object> optionalValues;
		
		public FieldSettings(String fieldName, List<object> optionalValues){
			this.fieldName = fieldName;
			this.optionalValues = optionalValues;
		}
	}
}



Now the usage can be:

DataCreator.generateRandomData(

'Lead', 5, new List<DataCreator.FieldSettings>{

new DataCreator.FieldSettings('Rating', new List<object>{'Hot', 'Warm'}),

new DataCreator.FieldSettings('Country', new List<object>{'Israel', 'Spain', 'US'}),

new DataCreator.FieldSettings('Status', new List<object>{'Open'})});




There is undoubtedly room for improvement. Support for extra field types, for instance, or more flexible options for data settings. I advise carrying it out as necessary.


One change that I would make is to have the method return both the list of new records and a parameter that lets us choose whether or not to insert the data at all. This way, whoever executes the function can easily extend data that the function generate.


public with sharing class DataCreator{
    public static List<sObject> generateRandomData(
        String objectType, 
        Integer totalRecords,
        List<FieldSettings> fieldsList,
        Boolean doInsert){
    
        List<sObject> newRecords = new List<sObject>();

        //Get all object fields
        SObjectType objectRef = Schema.getGlobalDescribe().get(objectType);
        Map<String, Schema.SObjectField> fieldMap = objectRef.getDescribe().fields.getMap();
        List<Schema.DescribeFieldResult> requireFields = new List<Schema.DescribeFieldResult>();
        
        for(Schema.SObjectField sField : fieldMap.values()){
            Schema.DescribeFieldResult dfr = sField.getDescribe();
            
            if(! dfr.isNillable() && dfr.isCreateable()){
                requireFields.add(dfr);
            }
        }
        
        for(Integer index = 0; index < totalRecords; index ++){
            SObject newRecord = objectRef.newSobject();
            
            for(FieldSettings fSettings : fieldsList){
                newRecord.put(fSettings.fieldName, fSettings.optionalValues.get(Integer.valueof((Math.random() * fSettings.optionalValues.size()))));
            }
        
            for(Schema.DescribeFieldResult dfr : requireFields){
                if(newRecord.get(dfr.getName()) == null){
            
                    if(dfr.getType() == Schema.DisplayType.Picklist || dfr.getType() == Schema.DisplayType.MultiPicklist){                      
                        newRecord.put(dfr.getName(), dfr.getPicklistValues().get(Integer.valueof((Math.random() * dfr.getPicklistValues().size()))).getValue());
                    }
                    else if(dfr.getType() == Schema.DisplayType.String || dfr.getType() == Schema.DisplayType.TextArea || dfr.getType() == Schema.DisplayType.URL){
                        newRecord.put(dfr.getName(), 'test1');
                    }
                    else if(dfr.getType() == Schema.DisplayType.Integer || dfr.getType() == Schema.DisplayType.Long || dfr.getType() == Schema.DisplayType.Double){
                        newRecord.put(dfr.getName(), 1);
                    }
                    else if(dfr.getType() == Schema.DisplayType.Date || dfr.getType() == Schema.DisplayType.DateTime){
                        newRecord.put(dfr.getName(), System.today());
                    }
                    else if(dfr.getType() == Schema.DisplayType.Boolean){
                        newRecord.put(dfr.getName(), false);
                    }
                    else if(dfr.getType() == Schema.DisplayType.REFERENCE) {
                    
                        set<String> sTypes = new set<String>();
                        
                        for(Schema.sObjectType refTo : dfr.getReferenceTo()){
                            sTypes.add(refTo.getDescribe().getName());
                        }
                        
                        if(sTypes.contains('User')){
                            newRecord.put(dfr.getName(), UserInfo.getUserId());
                        }
                        else if(sTypes.contains('Profile')){
                            //Special logic for profile as we cannot create profiles will use the profile of the running user
                            newRecord.put(dfr.getName(), UserInfo.getProfileId());
                        }
                    }
                    else{
                        //Unsupported type is require - need to improve the code
                        System.debug('Require field is not populated: ' + dfr.getName() + ' ' + dfr.getType());
                    }
                }
            }
            
            newRecords.add(newRecord);
        }
        
        
        if(doInsert){
            insert newRecords;
        }
        
        return newRecords;
    }
    
    
    public class FieldSettings{
        private String fieldName;
        private List<object> optionalValues;
        
        public FieldSettings(String fieldName, List<object> optionalValues){
            this.fieldName = fieldName;
            this.optionalValues = optionalValues;
        }
    }
}















Retire of Permission on Profiles

If you are working as a Salesforce admin/developer you've probably heard somewhere that Salesforce is planning to make a significant cha...