3

Is there a way from Apex to dynamically determine if a field is hidden from any page layout for the current Profile and Record Type?

Basically, I am customizing the Contact Merge functionality. I tried to list all fields dynamically. But the Standard Contact Merge apparently hide all fields that were hidden in the page. This is what I am trying to do.

1 Answer 1

3

You can use the User Interface API for this requirement.

You can run a very simple anonymous script to see what kind of data you'll get back:

Contact record = [SELECT Id FROM Contact LIMIT 1];
String endpoint = Url.getSalesforceBaseUrl().toExternalForm() +
    '/services/data/v41.0/ui-api/record-ui/' + record.Id;

HttpRequest request = new HttpRequest();
request.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId());
request.setEndpoint(endpoint);
request.setMethod('GET');

HttpResponse response = new Http().send(request);
system.debug(response);

For me, that data looks something like:

{
    "layoutUserStates": {
        /*irrelevant properties*/
    },
    "layouts": {
        "Contact": {
            "012000000000000AAA": {
                "Full": {
                    "View": {
                        /*irrelevant properties*/
                        "sections": [{
                            /*irrelevant properties*/
                            "layoutRows": [{
                                "layoutItems": [{
                                    /*irrelevant properties*/
                                    "layoutComponents": [{
                                        "apiName": "OwnerId",
                                        "componentType": "Field",
                                        "label": "Owner ID"
                                    }],
                                    /*irrelevant properties*/
                                }, /*more items*/]
                            }, /*more rows*/],
                            /*irrelevant properties*/
                        }, /*more sections*/]
                    }
                }
            }
        }
    },
    "objectInfos": {
        /*so much dang metadata*/
    },
    "records": {
        "003...": {
            "apiName": "Contact",
            "childRelationships": {},
            "fields": {
                "Account": {
                    "displayValue": "Edge Communications",
                    "value": {
                        "apiName": "Account",
                        "childRelationships": {},
                        "fields": {
                            "Id": {
                                "displayValue": null,
                                "value": "001..."
                            },
                            "Name": {
                                "displayValue": null,
                                "value": "Edge Communications"
                            }
                        },
                        "id": "001...",
                        "recordTypeInfo": null
                    }
                },
                "AccountId": {
                    "displayValue": null,
                    "value": "001..."
                },
                /*other fields*/
            },
            "id": "003...",
            "recordTypeInfo": null
        }
    }
}

You will need to define quite a lot of structure to deserialize this data but it can be done. Here is what mine looked like after taking a few shortcuts:

class ResponseBundle { final ResponseLayouts layouts; }
class ResponseLayouts { Map<String, Map<String, Map<String, Layout>>> Contact; }

class Layout { final List<Section> sections; }
class Section { final List<LayoutRow> layoutRows; }
class LayoutRow { final List<LayoutItem> layoutItems; }
class LayoutItem { final List<LayoutComponent> layoutComponents; }
class LayoutComponent { final String apiName, componentType, label; }

And then the deserialization looks like:

Contact record = [SELECT Id FROM Contact LIMIT 1];
Id recordTypeId = SObjectType.RecordType.getKeyPrefix().rightPad(15, '0') + 'AAA';
// RecordType not enabled for Contact in my org
// otherwise you would just use record.RecordTypeId
String endpoint = Url.getSalesforceBaseUrl().toExternalForm() +
    '/services/data/v41.0/ui-api/record-ui/' + record.Id;

HttpRequest request = new HttpRequest();
request.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId());
request.setEndpoint(endpoint);
request.setMethod('GET');

HttpResponse response = new Http().send(request);
ResponseBundle data = (ResponseBundle)JSON.deserialize(response.getBody(), ResponseBundle.class);
Layout fullView = data.layouts.Contact.get(recordTypeId).get('Full').get('View');

Set<String> apiNames = new Set<String>();
for (Section section : fullView.sections)
    for (LayoutRow layoutRow : section.layoutRows)
        for (LayoutItem layoutItem : layoutRow.layoutItems)
            for (LayoutComponent layoutComponent : layoutItem.layoutComponents)
                apiNames.add(layoutComponent.apiName);

Now, you may notice that there is a "data" section of the response and wonder if that gives you back the same fields. Turns out it does, but with a few extras (all the name-pointing parent objects in addition to the lookup Id. So you can shortcut this process further:

class ResponseBundle { final Map<String, Record> records; }
class Record { final Map<String, Field> fields; }
class Field { }

Id recordId = [SELECT Id FROM Contact LIMIT 1].Id;
String endpoint = Url.getSalesforceBaseUrl().toExternalForm() +
    '/services/data/v41.0/ui-api/record-ui/' + recordId;

HttpRequest request = new HttpRequest();
request.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId());
request.setEndpoint(endpoint);
request.setMethod('GET');

HttpResponse response = new Http().send(request);
ResponseBundle data = (ResponseBundle)JSON.deserialize(response.getBody(), ResponseBundle.class);
Set<String> dataFields = data.records.get(recordId).fields.keySet();

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .