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;
        }
    }
}















Input for Uploading Files in Web Component

To upload files through lightning web component, we can use one of two options:

  1. A dedicated component for files - lightning-file-upload.
  2. A lightning input with type: file.


The primary distinction is that the first is simpler to use and saves lines of code. Because the component already manages the uploading, error handling, and linking to the record.

However, the second option provides the developer with significantly more flexibility and is valuable in situations where the first option is insufficient. For example, if we want to do something else with the file, such as sending it by email or extracting data from it, or if we want to change the styling of the component.

In fact, it's the same trade-off as in many cases: use something that was already built but has limitation, or to invest more and develop something unique.


Another important consideration is the limitation related to file size and number of files that can be used. The maximum file size for the lightning-file-uploader is 2GB, which is sufficient for most cases. The lightning-input has no size limitation, but there are other considerations that might require attention (like payload size and heap size) depending on what we are doing with the file. Eventually with this option when we will need to process large files we need to use special logic in the code. For example, split the file to chunks.


Examples

We can use the following code to upload files in the first option. There is no need for Javascript code to process anything because the component already handles everything!


<template>
    <lightning-file-upload
        label="Attach File"
        name="fileUploader"
        record-id={recordId}
        multiple>
    </lightning-file-upload>    
</template>






As previously said, the component simplifies the procedure while allowing for some flexibility. Yes, we can limit the file types that can be used and provide a function that will be invoked after the component has finished uploading the file, however in many circumstances, this is insufficient.
In such cases, we should choose the second choice.


We can change our html code to the following. 

<template>
    <lightning-input
        label="Attach File"
        type="file"
        onchange={importFileChange}>

    </lightning-input>
</template>


Initially the UI for choose (or dragging) files will look exactly the same, however here we need to handle the processing ourself using the method onchange that calling javascript method. 

The example below demonstrates how to extract the file name and data, which is similar code for most use cases, then there should be unique code that process the file per the requirements. 

import { LightningElement, track, api } from 'lwc';

export default class FileUploaderDemo extends LightningElement {
    @api recordId;

    @track importFileName;
    @track importFile;


    importFileChange(event){
        const file = event.target.files[0];
       
        var reader = new FileReader()
        reader.onload = () => {
            this.importFile = reader.result.split(',')[1];
            this.importFileName = file.name;

            //Process the file data as needed
        }
        reader.readAsDataURL(file);
    }

}


We can send the file data to a server method for further processing, add custom validation for it and much more, we can also change its styling.

<template>
    <lightning-input
        label="Attach File"
        type="file"
        onchange={importFileChange}
        style="background-color:green;">
       
    </lightning-input>
</template>



Summary

The obvious choice is to use less code that should be as simple as possible, thus we'll want to utilize lighting-file-uploader as often as feasible, but the second option will be handy in the other circumstances. The idea is to use it only when truly unique functionality is required, rather than for easy applications such as improving the UI. 
Finally, keep in mind that writing code for uploading files can be  complex task that requires costly maintenance. As a result, it is advised to think carefully before choosing this way.


















Manage Profile Field Permissions with Apex

 



Recently, I encountered several cases where I needed to update profile permissions for fields in bulk. I was a bit disappointed to find that there are few free tools for such a task, and most of the ones I tested suffered from limitations of one kind or another. 

Because this is a recurring issue and also out of a desire to learn, I decided to try writing such a tool.

While exploring the options, I discovered that this can be done directly through Apex code. The permissions for the fields are stored in an object called FieldPermissions and users with setup permission can perform operations on the object.


The requirement was straightforward:

  • Option for selecting profiles and permissions.
  • Option for selecting a set of fields, including fields from different objects.
  • Checkboxes to modify the selected fields permissions for the profiles.
  • A save button, which will update the changes.


So I wrote a component (lwc) embedded in the Lightning Page that allows multiple selection of profiles or permission sets as well as multiple fields, editing and saving.

I would have liked to write that it is a simple process, but not this time. Managing permission is usually not an easy process.

Although it is about managing records in one object, there are special cases that require special attention. For example, you must not give write permission on a field formula, some standard fields that cannot receive permissions, and probably a few more that I haven't discovered yet.


I didn't publish the code for this process yet, as it is unstable, but I can share a few guidelines for anyone dealing with similar tasks:


  • Obviously, you cannot grant edit access to a field without also granting read access.
  • There are standard fields that cannot have permissions, for example, Id and CreatedBy. In most cases, we can identify those using the method isPermissionable from DescribeFieldResult
System.debug('Account.Rating isPermissionable: ' + Schema.getGlobalDescribe().get('Account').getDescribe().fields.getMap().get('Rating').getDescribe().isPermissionable());
System.debug('Account.Id isPermissionable: ' + Schema.getGlobalDescribe().get('Account').getDescribe().fields.getMap().get('Id').getDescribe().isPermissionable());

//01:44:58:103 USER_DEBUG [1]|DEBUG|Account.Rating isPermissionable: true
//01:44:58:123 USER_DEBUG [2]|DEBUG|Account.Id isPermissionable: false

  • There are other cases that the method isPermissionable doesn't cover; for example, you cannot provide permission for different sections of an Address field (e.g. BillingCity, BillingCountry), even though those fields are available in the object. Should give access only to the main field (BillingAddress).
  • Cannot provide edit access for formula fields.
  • Removing access for a field can be done either by deleting the FieldPermissions record or by setting Edit/Read both to false. In such a case, Salesforce will automatically delete it. It also means that when querying the existing permissions for fields with no access, you won't find a FieldPermissions record.
  • For my requirement, I added permissions only for fields, but a similar process can be done for object permissions and Apex/Pages permissions using the standard objects ObjectPermissions and SetupEntityAccess, respectively.
  • Cannot grant access for manage permission set (should be obvious)


Summary 

After dealing with such a process and understanding some of the difficulties involved, I can see why there are few such free quality tools. In my opinion, if you encounter such a need for mass update field permissions, the first recommendation is to try to use the existing tools. Either the Salesforce standard, even if sometimes it takes more time, or external tools.

Another option is to write a one-time script that will update/create relevant FieldPermissions records. 

List<FieldPermissions> newFieldsPermission = new List<FieldPermissions>();

newFieldsPermission.add(new FieldPermissions(
	PermissionsRead = true,
	PermissionsEdit = true,
	ParentId = 'ProfileOrPermssionSetId',
	SobjectType = 'Account',
	Field = 'Accunt.Rating')));

insert newFieldsPermission;


It is also possible to construct the relevant data in a csv and then import it to Salesforce as any other data. 






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...